From 0b7f33d665c4c96f742470fbf00271cb55b4a866 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Tue, 10 Dec 2024 10:47:39 +0100 Subject: [PATCH] Make maliciousSiteProtection webView agnostic --- .../app/browser/BrowserWebViewClientTest.kt | 4 +- .../browser/WebViewRequestInterceptorTest.kt | 74 +++++----- .../UrlExtractingWebViewClientTest.kt | 4 +- .../referencetests/DomainsReferenceTest.kt | 6 +- .../referencetests/SurrogatesReferenceTest.kt | 6 +- .../app/browser/BrowserWebViewClient.kt | 24 +++- .../app/browser/WebViewRequestInterceptor.kt | 8 +- .../app/browser/di/BrowserModule.kt | 4 +- .../UrlExtractingWebViewClient.kt | 4 +- ...lMaliciousSiteBlockerWebViewIntegration.kt | 112 +++++++++++++++ ...iciousSiteBlockerWebViewIntegrationTest.kt | 131 ++++++++++++++++++ .../MaliciousSiteBlockerWebViewIntegration.kt | 39 ++++++ .../api/MaliciousSiteProtection.kt | 17 +-- .../impl/RealMaliciousSiteProtection.kt | 93 +------------ .../impl/RealMaliciousSiteProtectionTest.kt | 116 ---------------- 15 files changed, 365 insertions(+), 277 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegration.kt create mode 100644 app/src/test/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegrationTest.kt create mode 100644 browser-api/src/main/java/com/duckduckgo/browser/api/MaliciousSiteBlockerWebViewIntegration.kt delete mode 100644 malicious-site-protection/malicious-site-protection-impl/src/test/kotlin/com/duckduckgo/malicioussiteprotection/impl/RealMaliciousSiteProtectionTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index a796fc873d99..ff942c065724 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -66,6 +66,7 @@ import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker import com.duckduckgo.browser.api.JsInjectorPlugin +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.browser.api.WebViewVersionProvider import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.CurrentTimeProvider @@ -79,7 +80,6 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Off import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.Unavailable import com.duckduckgo.history.api.NavigationHistory -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.user.agent.api.ClientBrandHintProvider @@ -153,7 +153,7 @@ class BrowserWebViewClientTest { private val mockUriLoadedManager: UriLoadedManager = mock() private val mockAndroidBrowserConfigFeature: AndroidBrowserConfigFeature = mock() private val mockAndroidFeaturesHeaderPlugin = AndroidFeaturesHeaderPlugin(mockDuckDuckGoUrlDetector, mockAndroidBrowserConfigFeature, mock()) - private val mockMaliciousSiteProtection: MaliciousSiteProtection = mock() + private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock() @UiThreadTest @Before diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt index 8bccdddac934..ad115fcf07c6 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/WebViewRequestInterceptorTest.kt @@ -42,11 +42,11 @@ import com.duckduckgo.app.trackerdetection.TrackerDetector import com.duckduckgo.app.trackerdetection.model.TrackerStatus import com.duckduckgo.app.trackerdetection.model.TrackerType import com.duckduckgo.app.trackerdetection.model.TrackingEvent +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.httpsupgrade.api.HttpsUpgrader -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import com.duckduckgo.privacy.config.api.Gpc import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc.Companion.GPC_HEADER import com.duckduckgo.request.filterer.api.RequestFilterer @@ -98,7 +98,7 @@ class WebViewRequestInterceptorTest { fakeToggle, fakeUserAllowListRepository, ) - private val mockMaliciousSiteProtection: MaliciousSiteProtection = mock() + private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock() private var webView: WebView = mock() @@ -130,7 +130,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) assertCancelledResponse(response) @@ -143,7 +143,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -157,7 +157,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -172,7 +172,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -187,7 +187,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) verify(mockHttpsUpgrader).upgrade(any()) @@ -203,7 +203,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -217,7 +217,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) verify(mockDuckPlayer).intercept(any(), any(), any()) @@ -231,7 +231,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -245,7 +245,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -259,7 +259,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) assertRequestCanContinueToLoad(response) @@ -272,7 +272,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "duckduckgo.com/a/b/c?q=123".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -286,7 +286,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "donttrack.us/a/b/c?q=123".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -300,7 +300,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "spreadprivacy.com/a/b/c?q=123".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -314,7 +314,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "duckduckhack.com/a/b/c?q=123".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -328,7 +328,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "privatebrowsingmyths.com/a/b/c?q=123".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -342,7 +342,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "duck.co/a/b/c?q=123".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -359,7 +359,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockListener, ) @@ -377,7 +377,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockListener, ) @@ -395,7 +395,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -418,7 +418,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -442,7 +442,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -457,7 +457,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -474,7 +474,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -492,7 +492,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -510,7 +510,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -528,7 +528,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -546,7 +546,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -563,7 +563,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -580,7 +580,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -597,7 +597,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -614,7 +614,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = null, webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = mockWebViewClientListener, ) @@ -674,7 +674,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -713,7 +713,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -735,7 +735,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) @@ -755,7 +755,7 @@ class WebViewRequestInterceptorTest { request = mockRequest, documentUri = "foo.com".toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/urlextraction/UrlExtractingWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/urlextraction/UrlExtractingWebViewClientTest.kt index b97b21da6bde..fd521dfdf4af 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/urlextraction/UrlExtractingWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/urlextraction/UrlExtractingWebViewClientTest.kt @@ -24,9 +24,9 @@ import com.duckduckgo.app.browser.* import com.duckduckgo.app.browser.certificates.rootstore.TrustedCertificateStore import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.cookies.api.CookieManagerProvider -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before @@ -51,7 +51,7 @@ class UrlExtractingWebViewClientTest { private val thirdPartyCookieManager: ThirdPartyCookieManager = mock() private val urlExtractor: DOMUrlExtractor = mock() private val mockWebView: WebView = mock() - private val mockMaliciousSiteProtection: MaliciousSiteProtection = mock() + private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock() @UiThreadTest @Before diff --git a/app/src/androidTest/java/com/duckduckgo/app/referencetests/DomainsReferenceTest.kt b/app/src/androidTest/java/com/duckduckgo/app/referencetests/DomainsReferenceTest.kt index e9efcba2c2b3..50f945aa1b23 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/referencetests/DomainsReferenceTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/referencetests/DomainsReferenceTest.kt @@ -52,12 +52,12 @@ import com.duckduckgo.app.trackerdetection.db.TdsCnameEntityDao import com.duckduckgo.app.trackerdetection.db.TdsDomainEntityDao import com.duckduckgo.app.trackerdetection.db.TdsEntityDao import com.duckduckgo.app.trackerdetection.db.WebTrackersBlockedDao +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.FileUtilities import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.httpsupgrade.api.HttpsUpgrader -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import com.duckduckgo.privacy.config.api.ContentBlocking import com.duckduckgo.privacy.config.api.Gpc import com.duckduckgo.privacy.config.api.TrackerAllowlist @@ -120,7 +120,7 @@ class DomainsReferenceTest(private val testCase: TestCase) { ) private val mockGpc: Gpc = mock() private val mockAdClickManager: AdClickManager = mock() - private val mockMaliciousSiteProtection: MaliciousSiteProtection = mock() + private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock() companion object { private val moshi = Moshi.Builder().add(ActionJsonAdapter()).build() @@ -200,7 +200,7 @@ class DomainsReferenceTest(private val testCase: TestCase) { request = mockRequest, documentUri = testCase.siteURL.toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) diff --git a/app/src/androidTest/java/com/duckduckgo/app/referencetests/SurrogatesReferenceTest.kt b/app/src/androidTest/java/com/duckduckgo/app/referencetests/SurrogatesReferenceTest.kt index 1615945a7efa..8d6e3015751f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/referencetests/SurrogatesReferenceTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/referencetests/SurrogatesReferenceTest.kt @@ -50,12 +50,12 @@ import com.duckduckgo.app.trackerdetection.db.TdsCnameEntityDao import com.duckduckgo.app.trackerdetection.db.TdsDomainEntityDao import com.duckduckgo.app.trackerdetection.db.TdsEntityDao import com.duckduckgo.app.trackerdetection.db.WebTrackersBlockedDao +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.FileUtilities import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.httpsupgrade.api.HttpsUpgrader -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import com.duckduckgo.privacy.config.api.ContentBlocking import com.duckduckgo.privacy.config.api.Gpc import com.duckduckgo.privacy.config.api.TrackerAllowlist @@ -116,7 +116,7 @@ class SurrogatesReferenceTest(private val testCase: TestCase) { private val mockGpc: Gpc = mock() private val mockAdClickManager: AdClickManager = mock() private val mockCloakedCnameDetector: CloakedCnameDetector = mock() - private val mockMaliciousSiteProtection: MaliciousSiteProtection = mock() + private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock() companion object { private val moshi = Moshi.Builder().add(ActionJsonAdapter()).build() @@ -189,7 +189,7 @@ class SurrogatesReferenceTest(private val testCase: TestCase) { request = mockRequest, documentUri = testCase.siteURL.toUri(), webView = webView, - maliciousSiteProtection = mockMaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration = mockMaliciousSiteProtection, webViewClientListener = null, ) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index cc4be064a52f..e97175e533b1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -66,6 +66,7 @@ import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker import com.duckduckgo.browser.api.JsInjectorPlugin +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.common.utils.CurrentTimeProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint @@ -76,7 +77,6 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On import com.duckduckgo.duckplayer.impl.DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH import com.duckduckgo.history.api.NavigationHistory -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.user.agent.api.ClientBrandHintProvider @@ -118,7 +118,7 @@ class BrowserWebViewClient @Inject constructor( private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector, private val uriLoadedManager: UriLoadedManager, private val androidFeaturesHeaderPlugin: AndroidFeaturesHeaderPlugin, - private val phishingAndMalwareDetector: MaliciousSiteProtection, + private val maliciousSiteProtectionWebViewIntegration: MaliciousSiteBlockerWebViewIntegration, ) : WebViewClient() { var webViewClientListener: WebViewClientListener? = null @@ -165,7 +165,15 @@ class BrowserWebViewClient @Inject constructor( Timber.v("shouldOverride webViewUrl: ${webView.url} URL: $url") webViewClientListener?.onShouldOverride() - if (phishingAndMalwareDetector.shouldOverrideUrlLoading(url, webView.url?.toUri(), isForMainFrame, isRedirect, onSiteBlockedAsync)) { + if (runBlocking { + maliciousSiteProtectionWebViewIntegration.shouldOverrideUrlLoading( + url, + webView.url?.toUri(), + isForMainFrame, + onSiteBlockedAsync, + ) + } + ) { // TODO (cbarreiro): Handle site blocked synchronously return true } @@ -419,7 +427,7 @@ class BrowserWebViewClient @Inject constructor( // See https://app.asana.com/0/0/1206159443951489/f (WebView limitations) if (it != ABOUT_BLANK && start == null) { start = currentTimeProvider.elapsedRealtime() - phishingAndMalwareDetector.onPageLoadStarted() + maliciousSiteProtectionWebViewIntegration.onPageLoadStarted() } handleMediaPlayback(webView, it) autoconsent.injectAutoconsent(webView, url) @@ -522,7 +530,13 @@ class BrowserWebViewClient @Inject constructor( loginDetector.onEvent(WebNavigationEvent.ShouldInterceptRequest(webView, request)) } Timber.v("Intercepting resource ${request.url} type:${request.method} on page $documentUrl") - requestInterceptor.shouldIntercept(request, webView, documentUrl?.toUri(), phishingAndMalwareDetector, webViewClientListener) + requestInterceptor.shouldIntercept( + request, + webView, + documentUrl?.toUri(), + maliciousSiteProtectionWebViewIntegration, + webViewClientListener, + ) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt index ee7c2e360da6..0adaad641815 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt @@ -29,13 +29,13 @@ import com.duckduckgo.app.trackerdetection.CloakedCnameDetector import com.duckduckgo.app.trackerdetection.TrackerDetector import com.duckduckgo.app.trackerdetection.model.TrackerStatus import com.duckduckgo.app.trackerdetection.model.TrackingEvent +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.common.utils.AppUrl import com.duckduckgo.common.utils.DefaultDispatcherProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.isHttp import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.httpsupgrade.api.HttpsUpgrader -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import com.duckduckgo.privacy.config.api.Gpc import com.duckduckgo.request.filterer.api.RequestFilterer import com.duckduckgo.user.agent.api.UserAgentProvider @@ -49,7 +49,7 @@ interface RequestInterceptor { request: WebResourceRequest, webView: WebView, documentUri: Uri?, - maliciousSiteProtection: MaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration: MaliciousSiteBlockerWebViewIntegration, webViewClientListener: WebViewClientListener?, ): WebResourceResponse? @@ -94,7 +94,7 @@ class WebViewRequestInterceptor( request: WebResourceRequest, webView: WebView, documentUri: Uri?, - maliciousSiteProtection: MaliciousSiteProtection, + maliciousSiteProtectionWebViewIntegration: MaliciousSiteBlockerWebViewIntegration, webViewClientListener: WebViewClientListener?, ): WebResourceResponse? { val url: Uri? = request.url @@ -103,7 +103,7 @@ class WebViewRequestInterceptor( // TODO (cbarreiro): Handle site blocked asynchronously } - maliciousSiteProtection.shouldIntercept(request, documentUri, onSiteBlockedAsync)?.let { + maliciousSiteProtectionWebViewIntegration.shouldIntercept(request, documentUri, onSiteBlockedAsync)?.let { // TODO (cbarreiro): Handle site blocked synchronously return it } diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index df9c76cb5487..0c910af68c97 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -72,6 +72,7 @@ import com.duckduckgo.app.surrogates.ResourceSurrogates import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator import com.duckduckgo.app.trackerdetection.CloakedCnameDetector import com.duckduckgo.app.trackerdetection.TrackerDetector +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.cookies.api.CookieManagerProvider import com.duckduckgo.cookies.api.DuckDuckGoCookieManager @@ -85,7 +86,6 @@ import com.duckduckgo.downloads.impl.FileDownloadCallback import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.experiments.api.VariantManager import com.duckduckgo.httpsupgrade.api.HttpsUpgrader -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.privacy.config.api.Gpc import com.duckduckgo.privacy.config.api.TrackingParameters @@ -123,7 +123,7 @@ class BrowserModule { @AppCoroutineScope appCoroutineScope: CoroutineScope, dispatcherProvider: DispatcherProvider, urlExtractor: DOMUrlExtractor, - maliciousSiteProtection: MaliciousSiteProtection, + maliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration, ): UrlExtractingWebViewClient { return UrlExtractingWebViewClient( webViewHttpAuthStore, diff --git a/app/src/main/java/com/duckduckgo/app/browser/urlextraction/UrlExtractingWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/urlextraction/UrlExtractingWebViewClient.kt index dd41f50332ce..63516ae4e464 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/urlextraction/UrlExtractingWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/urlextraction/UrlExtractingWebViewClient.kt @@ -28,9 +28,9 @@ import com.duckduckgo.app.browser.certificates.rootstore.CertificateValidationSt import com.duckduckgo.app.browser.certificates.rootstore.TrustedCertificateStore import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.cookies.api.CookieManagerProvider -import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import kotlinx.coroutines.* import timber.log.Timber @@ -43,7 +43,7 @@ class UrlExtractingWebViewClient( private val appCoroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, private val urlExtractor: DOMUrlExtractor, - private val maliciousSiteProtection: MaliciousSiteProtection, + private val maliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration, ) : WebViewClient() { var urlExtractionListener: UrlExtractionListener? = null diff --git a/app/src/main/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegration.kt b/app/src/main/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegration.kt new file mode 100644 index 000000000000..6bd1ed7183f3 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegration.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024 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.app.browser.webview + +import android.net.Uri +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import androidx.annotation.VisibleForTesting +import androidx.core.net.toUri +import com.duckduckgo.browser.api.MaliciousSiteBlockerWebViewIntegration +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection +import com.squareup.anvil.annotations.ContributesBinding +import java.net.URLDecoder +import javax.inject.Inject +import timber.log.Timber + +@ContributesBinding(AppScope::class) +class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor( + private val maliciousSiteProtection: MaliciousSiteProtection, +) : MaliciousSiteBlockerWebViewIntegration { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + val processedUrls = mutableListOf() + + override suspend fun shouldIntercept( + request: WebResourceRequest, + documentUri: Uri?, + onSiteBlockedAsync: () -> Unit, + ): WebResourceResponse? { + if (!maliciousSiteProtection.isFeatureEnabled) { + return null + } + val url = request.url.let { + if (it.fragment != null) { + it.buildUpon().fragment(null).build() + } else { + it + } + } + + val decodedUrl = URLDecoder.decode(url.toString(), "UTF-8").lowercase() + + if (processedUrls.contains(decodedUrl)) { + processedUrls.remove(decodedUrl) + Timber.tag("PhishingAndMalwareDetector").d("Already intercepted, skipping $decodedUrl") + return null + } + + if (request.isForMainFrame && decodedUrl.toUri() == documentUri) { + if (maliciousSiteProtection.isMalicious(decodedUrl.toUri(), onSiteBlockedAsync)) { + return WebResourceResponse(null, null, null) + } + processedUrls.add(decodedUrl) + } else if (isForIframe(request) && documentUri?.host == request.requestHeaders["Referer"]?.toUri()?.host) { + if (maliciousSiteProtection.isMalicious(decodedUrl.toUri(), onSiteBlockedAsync)) { + return WebResourceResponse(null, null, null) + } + processedUrls.add(decodedUrl) + } + return null + } + + override suspend fun shouldOverrideUrlLoading( + url: Uri, + webViewUrl: Uri?, + isForMainFrame: Boolean, + onSiteBlockedAsync: () -> Unit, + ): Boolean { + if (!maliciousSiteProtection.isFeatureEnabled) { + return false + } + val decodedUrl = URLDecoder.decode(url.toString(), "UTF-8").lowercase() + + if (processedUrls.contains(decodedUrl)) { + processedUrls.remove(decodedUrl) + Timber.tag("PhishingAndMalwareDetector").d("Already intercepted, skipping $decodedUrl") + return false + } + + if (isForMainFrame && decodedUrl.toUri() == webViewUrl) { + if (maliciousSiteProtection.isMalicious(decodedUrl.toUri(), onSiteBlockedAsync)) { + return true + } + processedUrls.add(decodedUrl) + } + return false + } + + private fun isForIframe(request: WebResourceRequest) = request.requestHeaders["Sec-Fetch-Dest"] == "iframe" || + request.url.path?.contains("/embed/") == true || + request.url.path?.contains("/iframe/") == true || + request.requestHeaders["Accept"]?.contains("text/html") == true + + override fun onPageLoadStarted() { + processedUrls.clear() + } +} diff --git a/app/src/test/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegrationTest.kt b/app/src/test/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegrationTest.kt new file mode 100644 index 000000000000..e1ee15280d88 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/browser/webview/RealMaliciousSiteBlockerWebViewIntegrationTest.kt @@ -0,0 +1,131 @@ +package com.duckduckgo.app.browser.webview + +import android.webkit.WebResourceRequest +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class RealMaliciousSiteBlockerWebViewIntegrationTest { + + private lateinit var testee: RealMaliciousSiteBlockerWebViewIntegration + private val maliciousSiteProtection: MaliciousSiteProtection = mock(MaliciousSiteProtection::class.java) + private val maliciousUri = "http://malicious.com".toUri() + private val exampleUri = "http://example.com".toUri() + + @Before + fun setup() { + testee = RealMaliciousSiteBlockerWebViewIntegration(maliciousSiteProtection) + whenever(maliciousSiteProtection.isFeatureEnabled).thenReturn(true) + } + + @Test + fun `shouldOverrideUrlLoading returns false when feature is disabled`() = runTest { + whenever(maliciousSiteProtection.isFeatureEnabled).thenReturn(false) + + val result = testee.shouldOverrideUrlLoading(exampleUri, null, true) {} + assertFalse(result) + } + + @Test + fun `shouldInterceptRequest returns null when feature is disabled`() = runTest { + val request = mock(WebResourceRequest::class.java) + whenever(request.url).thenReturn(exampleUri) + whenever(maliciousSiteProtection.isFeatureEnabled).thenReturn(false) + + val result = testee.shouldIntercept(request, null) {} + assertNull(result) + } + + @Test + fun `shouldOverrideUrlLoading returns false when url is already processed`() = runTest { + testee.processedUrls.add(exampleUri.toString()) + + val result = testee.shouldOverrideUrlLoading(exampleUri, exampleUri, true) {} + assertFalse(result) + } + + @Test + fun `shouldInterceptRequest returns result when feature is enabled, is malicious, and is mainframe`() = runTest { + val request = mock(WebResourceRequest::class.java) + whenever(request.url).thenReturn(maliciousUri) + whenever(request.isForMainFrame).thenReturn(true) + whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(true) + + val result = testee.shouldIntercept(request, maliciousUri) {} + assertNotNull(result) + } + + @Test + fun `shouldInterceptRequest returns result when feature is enabled, is malicious, and is iframe`() = runTest { + val request = mock(WebResourceRequest::class.java) + whenever(request.url).thenReturn(maliciousUri) + whenever(request.isForMainFrame).thenReturn(true) + whenever(request.requestHeaders).thenReturn(mapOf("Sec-Fetch-Dest" to "iframe")) + whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(true) + + val result = testee.shouldIntercept(request, maliciousUri) {} + assertNotNull(result) + } + + @Test + fun `shouldInterceptRequest returns null when feature is enabled, is malicious, and is not mainframe nor iframe`() = runTest { + val request = mock(WebResourceRequest::class.java) + whenever(request.url).thenReturn(maliciousUri) + whenever(request.isForMainFrame).thenReturn(false) + whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(true) + + val result = testee.shouldIntercept(request, maliciousUri) {} + assertNull(result) + } + + @Test + fun `shouldOverride returns false when feature is enabled, is malicious, and is not mainframe`() = runTest { + whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(true) + + val result = testee.shouldOverrideUrlLoading(maliciousUri, maliciousUri, false) {} + assertFalse(result) + } + + @Test + fun `shouldOverride returns true when feature is enabled, is malicious, and is mainframe`() = runTest { + whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(true) + + val result = testee.shouldOverrideUrlLoading(maliciousUri, maliciousUri, true) {} + assertTrue(result) + } + + @Test + fun `shouldOverride returns false when feature is enabled, is malicious, and not mainframe nor iframe`() = runTest { + whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(true) + + val result = testee.shouldOverrideUrlLoading(maliciousUri, maliciousUri, false) {} + assertFalse(result) + } + + @Test + fun `shouldOverride returns true when feature is enabled, is malicious, and is mainframe but webView has different host`() = runTest { + whenever(maliciousSiteProtection.isMalicious(any(), any())).thenReturn(true) + + val result = testee.shouldOverrideUrlLoading(maliciousUri, exampleUri, true) {} + assertFalse(result) + } + + @Test + fun `onPageLoadStarted clears processedUrls`() = runTest { + testee.processedUrls.add(exampleUri.toString()) + testee.onPageLoadStarted() + assertTrue(testee.processedUrls.isEmpty()) + } +} diff --git a/browser-api/src/main/java/com/duckduckgo/browser/api/MaliciousSiteBlockerWebViewIntegration.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/MaliciousSiteBlockerWebViewIntegration.kt new file mode 100644 index 000000000000..4b9a2e8c0b75 --- /dev/null +++ b/browser-api/src/main/java/com/duckduckgo/browser/api/MaliciousSiteBlockerWebViewIntegration.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 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.browser.api + +import android.net.Uri +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse + +interface MaliciousSiteBlockerWebViewIntegration { + + suspend fun shouldIntercept( + request: WebResourceRequest, + documentUri: Uri?, + onSiteBlockedAsync: () -> Unit, + ): WebResourceResponse? + + suspend fun shouldOverrideUrlLoading( + url: Uri, + webViewUrl: Uri?, + isForMainFrame: Boolean, + onSiteBlockedAsync: () -> Unit, + ): Boolean + + fun onPageLoadStarted() +} diff --git a/malicious-site-protection/malicious-site-protection-api/src/main/kotlin/com/duckduckgo/malicioussiteprotection/api/MaliciousSiteProtection.kt b/malicious-site-protection/malicious-site-protection-api/src/main/kotlin/com/duckduckgo/malicioussiteprotection/api/MaliciousSiteProtection.kt index 46111f9a7d20..0ebd2e4e6b26 100644 --- a/malicious-site-protection/malicious-site-protection-api/src/main/kotlin/com/duckduckgo/malicioussiteprotection/api/MaliciousSiteProtection.kt +++ b/malicious-site-protection/malicious-site-protection-api/src/main/kotlin/com/duckduckgo/malicioussiteprotection/api/MaliciousSiteProtection.kt @@ -17,23 +17,10 @@ package com.duckduckgo.malicioussiteprotection.api import android.net.Uri -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse interface MaliciousSiteProtection { - suspend fun shouldIntercept( - request: WebResourceRequest, - documentUri: Uri?, - onSiteBlockedAsync: () -> Unit, - ): WebResourceResponse? - fun onPageLoadStarted() + suspend fun isMalicious(url: Uri, onSiteBlockedAsync: () -> Unit): Boolean - fun shouldOverrideUrlLoading( - url: Uri, - webViewUrl: Uri?, - isForMainFrame: Boolean, - isRedirect: Boolean, - onSiteBlockedAsync: () -> Unit, - ): Boolean + val isFeatureEnabled: Boolean } diff --git a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/RealMaliciousSiteProtection.kt b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/RealMaliciousSiteProtection.kt index 0e307d5ade25..7e6a43b8fa56 100644 --- a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/RealMaliciousSiteProtection.kt +++ b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/RealMaliciousSiteProtection.kt @@ -17,10 +17,6 @@ package com.duckduckgo.malicioussiteprotection.impl import android.net.Uri -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import androidx.annotation.VisibleForTesting -import androidx.core.net.toUri import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.common.utils.DispatcherProvider @@ -29,7 +25,6 @@ import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding -import java.net.URLDecoder import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -45,7 +40,10 @@ class RealMaliciousSiteProtection @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : MaliciousSiteProtection, PrivacyConfigCallbackPlugin { - private var isFeatureEnabled = false + private var _isFeatureEnabled = false + override val isFeatureEnabled: Boolean + get() = _isFeatureEnabled + private var hashPrefixUpdateFrequency = 20L private var filterSetUpdateFrequency = 720L @@ -61,7 +59,7 @@ class RealMaliciousSiteProtection @Inject constructor( private fun loadToMemory() { appCoroutineScope.launch(dispatchers.io()) { - isFeatureEnabled = maliciousSiteProtectionFeature.self().isEnabled() + _isFeatureEnabled = maliciousSiteProtectionFeature.self().isEnabled() maliciousSiteProtectionFeature.self().getSettings()?.let { JSONObject(it).let { settings -> hashPrefixUpdateFrequency = settings.getLong("hashPrefixUpdateFrequency") @@ -71,86 +69,9 @@ class RealMaliciousSiteProtection @Inject constructor( } } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - val processedUrls = mutableListOf() - - private fun shouldIntercept(url: Uri, onSiteBlockedAsync: () -> Unit): Boolean { - Timber.tag("PhishingAndMalwareDetector").d("shouldIntercept $url") + override suspend fun isMalicious(url: Uri, onSiteBlockedAsync: () -> Unit): Boolean { + Timber.tag("MaliciousSiteProtection").d("isMalicious $url") // TODO (cbarreiro): Implement the logic to check if the URL is malicious return false } - - override suspend fun shouldIntercept( - request: WebResourceRequest, - documentUri: Uri?, - onSiteBlockedAsync: () -> Unit, - ): WebResourceResponse? { - if (!isFeatureEnabled) { - return null - } - val url = request.url.let { - if (it.fragment != null) { - it.buildUpon().fragment(null).build() - } else { - it - } - } - - val decodedUrl = URLDecoder.decode(url.toString(), "UTF-8").lowercase() - - if (processedUrls.contains(decodedUrl)) { - processedUrls.remove(decodedUrl) - Timber.tag("PhishingAndMalwareDetector").d("Already intercepted, skipping $decodedUrl") - return null - } - - if (request.isForMainFrame && decodedUrl.toUri() == documentUri) { - if (shouldIntercept(decodedUrl.toUri(), onSiteBlockedAsync)) { - return WebResourceResponse(null, null, null) - } - processedUrls.add(decodedUrl) - } else if (isForIframe(request) && documentUri?.host == request.requestHeaders["Referer"]?.toUri()?.host) { - if (shouldIntercept(decodedUrl.toUri(), onSiteBlockedAsync)) { - return WebResourceResponse(null, null, null) - } - processedUrls.add(decodedUrl) - } - return null - } - - override fun shouldOverrideUrlLoading( - url: Uri, - webViewUrl: Uri?, - isForMainFrame: Boolean, - isRedirect: Boolean, - onSiteBlockedAsync: () -> Unit, - ): Boolean { - if (!isFeatureEnabled) { - return false - } - val decodedUrl = URLDecoder.decode(url.toString(), "UTF-8").lowercase() - - if (processedUrls.contains(decodedUrl)) { - processedUrls.remove(decodedUrl) - Timber.tag("PhishingAndMalwareDetector").d("Already intercepted, skipping $decodedUrl") - return false - } - - if (isForMainFrame && decodedUrl.toUri() == webViewUrl) { - if (shouldIntercept(decodedUrl.toUri(), onSiteBlockedAsync)) { - return true - } - processedUrls.add(decodedUrl) - } - return false - } - - private fun isForIframe(request: WebResourceRequest) = request.requestHeaders["Sec-Fetch-Dest"] == "iframe" || - request.url.path?.contains("/embed/") == true || - request.url.path?.contains("/iframe/") == true || - request.requestHeaders["Accept"]?.contains("text/html") == true - - override fun onPageLoadStarted() { - processedUrls.clear() - } } diff --git a/malicious-site-protection/malicious-site-protection-impl/src/test/kotlin/com/duckduckgo/malicioussiteprotection/impl/RealMaliciousSiteProtectionTest.kt b/malicious-site-protection/malicious-site-protection-impl/src/test/kotlin/com/duckduckgo/malicioussiteprotection/impl/RealMaliciousSiteProtectionTest.kt deleted file mode 100644 index ff83e21b4c9d..000000000000 --- a/malicious-site-protection/malicious-site-protection-impl/src/test/kotlin/com/duckduckgo/malicioussiteprotection/impl/RealMaliciousSiteProtectionTest.kt +++ /dev/null @@ -1,116 +0,0 @@ -import android.annotation.SuppressLint -import android.net.Uri -import android.webkit.WebResourceRequest -import androidx.core.net.toUri -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory -import com.duckduckgo.feature.toggles.api.Toggle -import com.duckduckgo.malicioussiteprotection.impl.MaliciousSiteProtectionFeature -import com.duckduckgo.malicioussiteprotection.impl.RealMaliciousSiteProtection -import junit.framework.TestCase.assertTrue -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.* -import org.mockito.kotlin.whenever - -@SuppressLint("DenyListedApi") -@RunWith(AndroidJUnit4::class) -class RealMaliciousSiteProtectionTest { - - private val maliciousSiteProtectionFeature = FakeFeatureToggleFactory.create(MaliciousSiteProtectionFeature::class.java) - - private lateinit var testee: RealMaliciousSiteProtection - - @get:Rule - var coroutineTestRule = CoroutineTestRule() - - @Before - fun setup() { - testee = RealMaliciousSiteProtection( - coroutineTestRule.testDispatcherProvider, - maliciousSiteProtectionFeature, - true, - coroutineTestRule.testScope, - ) - } - - @Test - fun `shouldOverrideUrlLoading returns false when feature is disabled`() = runTest { - val url = mock(Uri::class.java) - whenever(url.toString()).thenReturn("http://example.com") - maliciousSiteProtectionFeature.self().setRawStoredState(Toggle.State(false)) - testee.onPrivacyConfigDownloaded() - - val result = testee.shouldOverrideUrlLoading(url, null, true, false) {} - assertFalse(result) - } - - @Test - fun `shouldOverrideUrlLoading returns false when for main frame`() = runTest { - val url = mock(Uri::class.java) - whenever(url.toString()).thenReturn("http://malicious.com") - maliciousSiteProtectionFeature.self().setRawStoredState(Toggle.State(true)) - testee.onPrivacyConfigDownloaded() - - val result = testee.shouldOverrideUrlLoading(url, "http://malicious.com".toUri(), true, false) {} - assertFalse(result) - } - - @Test - fun `shouldOverrideUrlLoading returns false when url is already processed`() = runTest { - val url = mock(Uri::class.java) - whenever(url.toString()).thenReturn("http://example.com") - maliciousSiteProtectionFeature.self().setRawStoredState(Toggle.State(true)) - testee.processedUrls.add("http://example.com") - testee.onPrivacyConfigDownloaded() - - val result = testee.shouldOverrideUrlLoading(url, "http://example.com".toUri(), true, false) {} - assertFalse(result) - } - - @Test - fun `shouldOverrideUrlLoading returns false when for iframe`() = runTest { - val url = mock(Uri::class.java) - whenever(url.toString()).thenReturn("http://malicious.com") - maliciousSiteProtectionFeature.self().setRawStoredState(Toggle.State(true)) - testee.onPrivacyConfigDownloaded() - - val result = testee.shouldOverrideUrlLoading(url, "http://example.com".toUri(), false, false) {} - assertFalse(result) - } - - @Test - fun `shouldInterceptRequest returns null when feature is disabled`() = runTest { - val request = mock(WebResourceRequest::class.java) - whenever(request.url).thenReturn("http://example.com".toUri()) - maliciousSiteProtectionFeature.self().setRawStoredState(Toggle.State(false)) - testee.onPrivacyConfigDownloaded() - - val result = testee.shouldIntercept(request, null) {} - assertNull(result) - } - - @Test - fun `shouldInterceptRequest returns null when feature is enabled`() = runTest { - val request = mock(WebResourceRequest::class.java) - whenever(request.url).thenReturn("http://malicious.com".toUri()) - maliciousSiteProtectionFeature.self().setRawStoredState(Toggle.State(true)) - testee.onPrivacyConfigDownloaded() - - val result = testee.shouldIntercept(request, "http://malicious.com".toUri()) {} - assertNull(result) - } - - @Test - fun `onPageLoadStarted clears processedUrls`() = runTest { - testee.processedUrls.add("http://example.com") - testee.onPageLoadStarted() - assertTrue(testee.processedUrls.isEmpty()) - } -}