Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Malicious site protection - extract special error page navigation logic #3624

Merged
Show file tree
Hide file tree
Changes from 5 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
108 changes: 101 additions & 7 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// MaliciousSiteProtectionManager.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// 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.
//

import Foundation

final class MaliciousSiteProtectionManager: MaliciousSiteDetecting {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation of this class will come in a subsequent PR.
This was used to mock the BSK library and assert that the special error page was rendered.


func evaluate(_ url: URL) async -> ThreatKind? {
try? await Task.sleep(interval: 0.3)
return .none
}

}

// MARK: - To Remove

// These entities are copied from BSK and they will be used to mock the library
import SpecialErrorPages

protocol MaliciousSiteDetecting {
func evaluate(_ url: URL) async -> ThreatKind?
}

public enum ThreatKind: String, CaseIterable, CustomStringConvertible {
public var description: String { rawValue }

case phishing
case malware
}

public extension ThreatKind {

var errorPageType: SpecialErrorKind {
switch self {
case .malware: .phishing // WIP in BSK
case .phishing: .phishing
}
}

}
119 changes: 119 additions & 0 deletions DuckDuckGo/SpecialErrorPage/SpecialErrorPageInterfaces.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// SpecialErrorPageInterfaces.swift
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should put all these protocols under this umbrella file - for me personally it makes things confusing - I'm fine with putting separate files into group though named like this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me!

// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// 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.
//

import Foundation
import WebKit
import Common
import SpecialErrorPages

// MARK: - WebViewNavigationHandling

/// A protocol that defines methods for handling navigation events of `WKWebView`.
protocol WebViewNavigationHandling: AnyObject {
/// Decides whether to cancel navigation to prevent opening a site and show a special error page based on the specified action information.
///
/// - Parameters:
/// - navigationAction: Details about the action that triggered the navigation request.
/// - webView: The web view from which the navigation request began.
/// - Returns: A Boolean value that indicates whether the navigation action was handled.
func handleSpecialErrorNavigation(navigationAction: WKNavigationAction, webView: WKWebView) async -> Bool

/// Handles authentication challenges received by the web view.
///
/// - Parameters:
/// - webView: The web view that receives the authentication challenge.
/// - challenge: The authentication challenge.
/// - completionHandler: A completion handler block to execute with the response.
func handleWebView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

/// Handles failures during provisional navigation.
///
/// - Parameters:
/// - webView: The `WKWebView` instance that failed the navigation.
/// - navigation: The navigation object for the operation.
/// - error: The error that occurred.
func handleWebView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WebViewNavigation, withError error: NSError)

/// Handles the successful completion of a navigation in the web view.
///
/// - Parameters:
/// - webView: The web view that loaded the content.
/// - navigation: The navigation object that finished.
func handleWebView(_ webView: WKWebView, didFinish navigation: WebViewNavigation)
}

// MARK: - SpecialErrorPageActionHandler

/// A type that defines actions for handling special error pages.
///
/// This protocol is intended to be adopted by types that need to manage user interactions
/// with special error pages, such as navigating to a site, leaving a site, or presenting
/// advanced information related to the error.
protocol SpecialErrorPageActionHandler {
/// Handles the action of navigating to the site associated with the error page
func visitSite()

/// Handles the action of leaving the site associated with the error page
func leaveSite()

/// Handles the action of requesting more detailed information about the error
func advancedInfoPresented()
}

// MARK: - BaseSpecialErrorPageNavigationHandling

/// A type that defines the base functionality for handling navigation related to special error pages.
protocol BaseSpecialErrorPageNavigationHandling: AnyObject {
/// The delegate that handles navigation actions for special error pages.
var delegate: SpecialErrorPageNavigationDelegate? { get set }

/// A Boolean value indicating whether the special error page is currently visible.
var isSpecialErrorPageVisible: Bool { get }

/// The URL that failed to load, if any.
var failedURL: URL? { get }

/// Attaches a web view to the special error page handling.
func attachWebView(_ webView: WKWebView)

/// Sets the user script for the special error page.
func setUserScript(_ userScript: SpecialErrorPageUserScript?)
}

typealias SpecialErrorPageNavigationHandling = BaseSpecialErrorPageNavigationHandling & WebViewNavigationHandling & SpecialErrorPageUserScriptDelegate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we can rename these to:
SpecialErrorPageNavigationHandling -> SpecialErrorPageManaging
BaseSpecialErrorPageNavigationHandling -> SpecialErrorPageContextHandling/SpecialErrorPageFlowHandling (as it is mostly responsible for state handling and setup)? Base prefix doesn't tell much

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I wasn’t fan of “Base” as well but couldn’t find a better name. :)


// MARK: - SpecialErrorPageNavigationDelegate

/// A delegate for handling navigation actions related to special error pages.
protocol SpecialErrorPageNavigationDelegate: AnyObject {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this protocol above BaseSpecialError protocol where it is already used?

/// Asks the delegate to close the special error page tab when the web view can't navigate back.
func closeSpecialErrorPageTab()
}

// MARK: - WebViewNavigation

/// For testing purposes.
protocol WebViewNavigation {}

// Used in tests. WKNavigation() crashes on deinit when initialising it manually.
// As workaround we used to Swizzle the implementation of deinit in tests.
// The problem with that approach is that when running different test suites it is possible that unrelated tests re-set the original implementation of deinit while other tests are running.
// This cause the app to crash as the original implementation is executed.
// Defining a protocol for WKNavigation and using mocks such as DummyWKNavigation in tests resolves the problem.
extension WKNavigation: WebViewNavigation {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// SpecialErrorPageNavigationHandler+MaliciousSite.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// 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.
//

import Foundation
import BrowserServicesKit
import Core
import SpecialErrorPages
import WebKit

enum MaliciousSiteProtectionNavigationResult: Equatable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class will encapsulate MaliciousSiteProtection logic to evaluate if a site is malicious and will handle its internal state to bypass URLs.

case navigationHandled(MaliciousSiteProtectionNavigationHandlerModel)
case navigationNotHandled
}

struct MaliciousSiteProtectionNavigationHandlerModel: Equatable {
let error: SpecialErrorData
let url: URL
}

protocol MaliciousSiteProtectionNavigationHandling: AnyObject {
/// Decides whether to cancel navigation to prevent opening the YouTube app from the web view.
///
/// - Parameters:
/// - navigationAction: The navigation action to evaluate.
/// - webView: The web view where navigation is occurring.
/// - Returns: `true` if the navigation should be canceled, `false` otherwise.
func handleMaliciousSiteProtectionNavigation(for navigationAction: WKNavigationAction, webView: WKWebView) async -> MaliciousSiteProtectionNavigationResult
}

final class MaliciousSiteProtectionNavigationHandler {
private let maliciousSiteProtectionManager: MaliciousSiteDetecting
private let storageCache: StorageCache

init(
maliciousSiteProtectionManager: MaliciousSiteDetecting = MaliciousSiteProtectionManager(),
storageCache: StorageCache = AppDependencyProvider.shared.storageCache
) {
self.maliciousSiteProtectionManager = maliciousSiteProtectionManager
self.storageCache = storageCache
}
}

// MARK: - MaliciousSiteProtectionNavigationHandling

extension MaliciousSiteProtectionNavigationHandler: MaliciousSiteProtectionNavigationHandling {

@MainActor
func handleMaliciousSiteProtectionNavigation(for navigationAction: WKNavigationAction, webView: WKWebView) async -> MaliciousSiteProtectionNavigationResult {
// Implement logic to use `maliciousSiteProtectionManager.evaluate(url)`
// Return navigationNotHandled for the time being
return .navigationNotHandled
}

}

// MARK: - SpecialErrorPageActionHandler

extension MaliciousSiteProtectionNavigationHandler: SpecialErrorPageActionHandler {

func visitSite() {
// Fire Pixel
}

func leaveSite() {
// Fire Pixel
}

func advancedInfoPresented() {
// Fire Pixel
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// SpecialErrorPageNavigationHandler+SSL.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// 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.
//

import Foundation
import Common
import BrowserServicesKit
import SpecialErrorPages
import Core

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class encapsulate the SSL logic

struct SSLSpecialError {
let url: URL
let type: SSLErrorType
let errorData: SpecialErrorData
}

protocol SSLSpecialErrorPageNavigationHandling {
func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

func makeNewRequestURLAndSpecialErrorDataIfEnabled(error: NSError) -> SSLSpecialError?

func errorPageVisited(errorType: SSLErrorType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove empty lines

}

final class SSLErrorPageNavigationHandler {
private var shouldBypassSSLError = false

private let urlCredentialCreator: URLCredentialCreating
private let storageCache: StorageCache
private let featureFlagger: FeatureFlagger

init(
urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(),
storageCache: StorageCache = AppDependencyProvider.shared.storageCache,
featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger
) {
self.urlCredentialCreator = urlCredentialCreator
self.storageCache = storageCache
self.featureFlagger = featureFlagger
}
}

// MARK: - SSLSpecialErrorPageNavigationHandling

extension SSLErrorPageNavigationHandler: SSLSpecialErrorPageNavigationHandling {

func handleServerTrustChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard shouldBypassSSLError,
let credential = urlCredentialCreator.urlCredentialFrom(trust: challenge.protectionSpace.serverTrust) else {
completionHandler(.performDefaultHandling, nil)
return
}
shouldBypassSSLError = false
completionHandler(.useCredential, credential)
}

func makeNewRequestURLAndSpecialErrorDataIfEnabled(error: NSError) -> SSLSpecialError? {
guard featureFlagger.isFeatureOn(.sslCertificatesBypass),
error.code == NSURLErrorServerCertificateUntrusted,
let errorCode = error.userInfo["_kCFStreamErrorCodeKey"] as? Int32,
let failedURL = error.failedUrl else {
return nil
}

let errorType = SSLErrorType.forErrorCode(Int(errorCode))
let errorData = SpecialErrorData(kind: .ssl,
errorType: errorType.rawValue,
domain: failedURL.host,
eTldPlus1: storageCache.tld.eTLDplus1(failedURL.host))

return SSLSpecialError(url: failedURL, type: errorType, errorData: errorData)
}

func errorPageVisited(errorType: SSLErrorType) {
Pixel.fire(pixel: .certificateWarningDisplayed(errorType.rawParameter))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could inject the fire function and add tests to ensure Pixels are fired.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh I don't mind leaving it as it is for now

}

}

// MARK: - SSLErrorPageNavigationHandler

extension SSLErrorPageNavigationHandler: SpecialErrorPageActionHandler {

func leaveSite() {
Pixel.fire(pixel: .certificateWarningLeaveClicked)
}

func visitSite() {
shouldBypassSSLError = true
}

func advancedInfoPresented() {
Pixel.fire(pixel: .certificateWarningAdvancedClicked)
}

}
Loading
Loading