Skip to content

Commit

Permalink
Add feed to isMalicious return type and integrate with error page
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisBarreiro committed Jan 31, 2025
1 parent 6ea4903 commit 31ef2ac
Show file tree
Hide file tree
Showing 24 changed files with 346 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ import com.duckduckgo.app.browser.trafficquality.AndroidFeaturesHeaderPlugin
import com.duckduckgo.app.browser.trafficquality.CustomHeaderAllowedChecker
import com.duckduckgo.app.browser.trafficquality.remote.AndroidFeaturesHeaderProvider
import com.duckduckgo.app.browser.uriloaded.UriLoadedManager
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.global.model.Site
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.statistics.pixels.Pixel
Expand Down Expand Up @@ -164,7 +163,6 @@ class BrowserWebViewClientTest {
mockFeaturesHeaderProvider,
mock(),
)
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()
private val mockDuckChat: DuckChat = mock()

@UiThreadTest
Expand Down Expand Up @@ -211,7 +209,6 @@ class BrowserWebViewClientTest {
whenever(currentTimeProvider.elapsedRealtime()).thenReturn(0)
whenever(webViewVersionProvider.getMajorVersion()).thenReturn("1")
whenever(deviceInfo.appVersion).thenReturn("1")
whenever(mockMaliciousSiteProtection.shouldOverrideUrlLoading(any(), any(), any())).thenReturn(false)
}

@UiThreadTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.test.annotation.UiThreadTest
import com.duckduckgo.adclick.api.AdClickManager
import com.duckduckgo.app.browser.useragent.provideUserAgentOverridePluginPoint
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FakeMaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FeatureToggleFake
import com.duckduckgo.app.fakes.UserAgentFake
import com.duckduckgo.app.fakes.UserAllowListRepositoryFake
Expand Down Expand Up @@ -98,7 +99,7 @@ class WebViewRequestInterceptorTest {
fakeToggle,
fakeUserAllowListRepository,
)
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()
private val mockMaliciousSiteBlockerWebViewIntegration: MaliciousSiteBlockerWebViewIntegration = FakeMaliciousSiteBlockerWebViewIntegration()

private var webView: WebView = mock()

Expand All @@ -119,7 +120,7 @@ class WebViewRequestInterceptorTest {
cloakedCnameDetector = mockCloakedCnameDetector,
requestFilterer = mockRequestFilterer,
duckPlayer = mockDuckPlayer,
maliciousSiteBlockerWebViewIntegration = mockMaliciousSiteProtection,
maliciousSiteBlockerWebViewIntegration = mockMaliciousSiteBlockerWebViewIntegration,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ 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.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.cookies.api.CookieManagerProvider
import kotlinx.coroutines.test.TestScope
Expand All @@ -51,7 +50,6 @@ class UrlExtractingWebViewClientTest {
private val thirdPartyCookieManager: ThirdPartyCookieManager = mock()
private val urlExtractor: DOMUrlExtractor = mock()
private val mockWebView: WebView = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()

@UiThreadTest
@Before
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.app.fakes

import android.net.Uri
import android.webkit.WebResourceRequest
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData.Safe
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.MaliciousStatus

class FakeMaliciousSiteBlockerWebViewIntegration : MaliciousSiteBlockerWebViewIntegration {
override suspend fun shouldIntercept(
request: WebResourceRequest,
documentUri: Uri?,
confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit,
): IsMaliciousViewData {
return Safe
}

override fun shouldOverrideUrlLoading(
url: Uri,
isForMainFrame: Boolean,
confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit,
): IsMaliciousViewData {
return Safe
}

override fun onPageLoadStarted() {
// no-op
}

override fun onSiteExempted(
url: Uri,
feed: Feed,
) {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.duckduckgo.adclick.api.AdClickManager
import com.duckduckgo.app.browser.WebViewRequestInterceptor
import com.duckduckgo.app.browser.useragent.provideUserAgentOverridePluginPoint
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FakeMaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FeatureToggleFake
import com.duckduckgo.app.fakes.UserAgentFake
import com.duckduckgo.app.fakes.UserAllowListRepositoryFake
Expand Down Expand Up @@ -120,7 +121,7 @@ class DomainsReferenceTest(private val testCase: TestCase) {
)
private val mockGpc: Gpc = mock()
private val mockAdClickManager: AdClickManager = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = FakeMaliciousSiteBlockerWebViewIntegration()

companion object {
private val moshi = Moshi.Builder().add(ActionJsonAdapter()).build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.duckduckgo.adclick.api.AdClickManager
import com.duckduckgo.app.browser.WebViewRequestInterceptor
import com.duckduckgo.app.browser.useragent.provideUserAgentOverridePluginPoint
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FakeMaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FeatureToggleFake
import com.duckduckgo.app.fakes.UserAgentFake
import com.duckduckgo.app.fakes.UserAllowListRepositoryFake
Expand Down Expand Up @@ -116,7 +117,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: MaliciousSiteBlockerWebViewIntegration = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = FakeMaliciousSiteBlockerWebViewIntegration()

companion object {
private val moshi = Moshi.Builder().add(ActionJsonAdapter()).build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.js.messaging.api.JsMessageCallback
import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.navigation.api.GlobalActivityStarter.DeeplinkActivityParams
Expand Down Expand Up @@ -1341,7 +1342,7 @@ class BrowserTabFragment :
errorView.errorLayout.show()
}

private fun showMaliciousWarning(url: Uri) {
private fun showMaliciousWarning(url: Uri, feed: Feed) {
webViewContainer.gone()
newBrowserTab.newTabLayout.gone()
newBrowserTab.newTabContainerLayout.gone()
Expand All @@ -1352,8 +1353,8 @@ class BrowserTabFragment :
webView?.onPause()
webView?.hide()
webView?.stopLoading()
maliciousWarningView.bind { action ->
viewModel.onMaliciousSiteUserAction(action, url)
maliciousWarningView.bind(feed) { action ->
viewModel.onMaliciousSiteUserAction(action, url, feed)
}
maliciousWarningView.show()
binding.focusDummy.requestFocus()
Expand Down Expand Up @@ -1729,7 +1730,7 @@ class BrowserTabFragment :
)

is Command.WebViewError -> showError(it.errorType, it.url)
is Command.ShowWarningMaliciousSite -> showMaliciousWarning(it.url)
is Command.ShowWarningMaliciousSite -> showMaliciousWarning(it.url, it.feed)
is Command.HideWarningMaliciousSite -> hideMaliciousWarning()
is Command.EscapeMaliciousSite -> onEscapeMaliciousSite()
is Command.BypassMaliciousSiteWarning -> onBypassMaliciousWarning(it.url)
Expand Down
37 changes: 24 additions & 13 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.ui.AutomaticFireproofSetting.ASK
import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchOptionHandler
import com.duckduckgo.app.global.events.db.UserEventKey
import com.duckduckgo.app.global.events.db.UserEventsStore
import com.duckduckgo.app.global.model.MaliciousSiteStatus
import com.duckduckgo.app.global.model.PrivacyShield
import com.duckduckgo.app.global.model.Site
import com.duckduckgo.app.global.model.SiteFactory
Expand Down Expand Up @@ -294,6 +295,9 @@ import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.js.messaging.api.JsCallbackData
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.newtabpage.impl.pixels.NewTabPixels
import com.duckduckgo.privacy.config.api.AmpLinkInfo
import com.duckduckgo.privacy.config.api.AmpLinks
Expand Down Expand Up @@ -1862,6 +1866,7 @@ class BrowserTabViewModel @Inject constructor(
fun onMaliciousSiteUserAction(
action: MaliciousSiteBlockedWarningLayout.Action,
url: Uri,
feed: Feed,
) {
when (action) {
LeaveSite -> {
Expand All @@ -1874,7 +1879,7 @@ class BrowserTabViewModel @Inject constructor(
browserShowing = true,
showPrivacyShield = HighlightableButton.Visible(enabled = true),
)
addExemptedMaliciousUrlToMemory(url)
addExemptedMaliciousUrlToMemory(url, feed)
}
}
}
Expand Down Expand Up @@ -3153,17 +3158,23 @@ class BrowserTabViewModel @Inject constructor(
command.postValue(WebViewError(errorType, url))
}

override fun onReceivedMaliciousSiteWarning(url: Uri) {
override fun onReceivedMaliciousSiteWarning(url: Uri, feed: Feed, exempted: Boolean) {
// TODO (cbarreiro): Fire pixel
loadingViewState.postValue(currentLoadingViewState().copy(isLoading = false, progress = 100, url = url.toString()))
browserViewState.postValue(
currentBrowserViewState().copy(
browserShowing = false,
showPrivacyShield = HighlightableButton.Visible(enabled = false),
maliciousSiteDetected = true,
),
)
command.postValue(ShowWarningMaliciousSite(url))
site?.maliciousSiteStatus = when (feed) {
MALWARE -> MaliciousSiteStatus.MALWARE
PHISHING -> MaliciousSiteStatus.PHISHING
}
if (!exempted) {
loadingViewState.postValue(currentLoadingViewState().copy(isLoading = false, url = url.toString()))
browserViewState.postValue(
currentBrowserViewState().copy(
browserShowing = false,
showPrivacyShield = HighlightableButton.Visible(enabled = false),
maliciousSiteDetected = true,
),
)
command.postValue(ShowWarningMaliciousSite(url, feed))
}
}

override fun recordErrorCode(
Expand Down Expand Up @@ -3756,8 +3767,8 @@ class BrowserTabViewModel @Inject constructor(
command.value = SetOnboardingDialogBackground(getBackgroundResource(lightModeEnabled))
}

fun addExemptedMaliciousUrlToMemory(url: Uri) {
maliciousSiteBlockerWebViewIntegration.onSiteExempted(url)
fun addExemptedMaliciousUrlToMemory(url: Uri, feed: Feed) {
maliciousSiteBlockerWebViewIntegration.onSiteExempted(url, feed)
}

private fun getBackgroundResource(lightModeEnabled: Boolean): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.duckduckgo.app.browser.model.BasicAuthenticationRequest
import com.duckduckgo.app.global.model.Site
import com.duckduckgo.app.surrogates.SurrogateResponse
import com.duckduckgo.app.trackerdetection.model.TrackingEvent
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions

interface WebViewClientListener {
Expand Down Expand Up @@ -95,7 +96,7 @@ interface WebViewClientListener {
fun linkOpenedInNewTab(): Boolean
fun isActiveTab(): Boolean
fun onReceivedError(errorType: WebViewErrorResponse, url: String)
fun onReceivedMaliciousSiteWarning(url: Uri)
fun onReceivedMaliciousSiteWarning(url: Uri, feed: Feed, exempted: Boolean)
fun recordErrorCode(error: String, url: String)
fun recordHttpErrorCode(statusCode: Int, url: String)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import android.webkit.WebView
import androidx.annotation.WorkerThread
import com.duckduckgo.adclick.api.AdClickManager
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData.MaliciousSite
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData.Safe
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData.WaitForConfirmation
import com.duckduckgo.app.privacy.db.PrivacyProtectionCountDao
import com.duckduckgo.app.privacy.model.TrustedSites
import com.duckduckgo.app.surrogates.ResourceSurrogates
Expand All @@ -36,6 +40,9 @@ 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.Feed
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.MaliciousStatus
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.MaliciousStatus.Malicious
import com.duckduckgo.privacy.config.api.Gpc
import com.duckduckgo.request.filterer.api.RequestFilterer
import com.duckduckgo.user.agent.api.UserAgentProvider
Expand Down Expand Up @@ -107,12 +114,9 @@ class WebViewRequestInterceptor(
val url: Uri? = request.url

maliciousSiteBlockerWebViewIntegration.shouldIntercept(request, documentUri) { isMalicious ->
if (isMalicious) {
handleSiteBlocked(webViewClientListener, url)
}
}?.let {
handleSiteBlocked(webViewClientListener, url)
return it
handleConfirmationCallback(isMalicious, webViewClientListener, url)
}.let {
if (shouldBlock(it, webViewClientListener, url)) return WebResourceResponse(null, null, null)
}

if (requestFilterer.shouldFilterOutRequest(request, documentUri.toString())) return WebResourceResponse(null, null, null)
Expand Down Expand Up @@ -166,6 +170,17 @@ class WebViewRequestInterceptor(
return getWebResourceResponse(request, documentUri, webViewClientListener)
}

override fun shouldOverrideUrlLoading(webViewClientListener: WebViewClientListener?, url: Uri, isForMainFrame: Boolean): Boolean {
maliciousSiteBlockerWebViewIntegration.shouldOverrideUrlLoading(
url,
isForMainFrame,
) { isMalicious ->
handleConfirmationCallback(isMalicious, webViewClientListener, url)
}.let {
return shouldBlock(it, webViewClientListener, url)
}
}

override suspend fun shouldInterceptFromServiceWorker(
request: WebResourceRequest?,
documentUrl: Uri?,
Expand All @@ -180,24 +195,32 @@ class WebViewRequestInterceptor(
return getWebResourceResponse(request, documentUrl, null)
}

override fun shouldOverrideUrlLoading(webViewClientListener: WebViewClientListener?, url: Uri, isForMainFrame: Boolean): Boolean {
if (maliciousSiteBlockerWebViewIntegration.shouldOverrideUrlLoading(
url,
isForMainFrame,
) { isMalicious ->
if (isMalicious) {
handleSiteBlocked(webViewClientListener, url)
}
private fun shouldBlock(
result: IsMaliciousViewData,
webViewClientListener: WebViewClientListener?,
url: Uri?,
): Boolean {
when (result) {
WaitForConfirmation, Safe -> return false
is MaliciousSite -> {
handleSiteBlocked(webViewClientListener, url, result.feed, result.exempted)
return !result.exempted
}
) {
handleSiteBlocked(webViewClientListener, url)
return true
}
return false
}

private fun handleSiteBlocked(webViewClientListener: WebViewClientListener?, url: Uri?) {
url?.let { webViewClientListener?.onReceivedMaliciousSiteWarning(it) }
private fun handleConfirmationCallback(
isMalicious: MaliciousStatus,
webViewClientListener: WebViewClientListener?,
url: Uri?,
) {
if (isMalicious is Malicious) {
handleSiteBlocked(webViewClientListener, url, isMalicious.feed, false)
}
}

private fun handleSiteBlocked(webViewClientListener: WebViewClientListener?, url: Uri?, feed: Feed, exempted: Boolean) {
url?.let { webViewClientListener?.onReceivedMaliciousSiteWarning(it, feed, exempted) }
}

private fun getWebResourceResponse(
Expand Down
Loading

0 comments on commit 31ef2ac

Please sign in to comment.