-
Notifications
You must be signed in to change notification settings - Fork 426
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
Changes from 5 commits
62fe8f5
f11301e
43d6335
4d4f428
c3815f5
cdf5546
b77eb5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
|
||
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 | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
// | ||
// SpecialErrorPageInterfaces.swift | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps we can rename these to: There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
|
||
} |
There was a problem hiding this comment.
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.