diff --git a/Package.resolved b/Package.resolved index 46c243c63..8a2d1a4ad 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "aa279a3b006a0b1e009707311283c7fcaed24fb7", - "version" : "4.39.0" + "revision" : "254b23cf292140498650421bb31fd05740f4579b", + "version" : "4.40.0" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "6dd7d696d4e666cedb2f1890a46fe53615226646", - "version" : "8.4.2" + "revision" : "c8e895c8fd50dc76e8d8dc827a636ad77b7f46ff", + "version" : "9.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "51e2b46f413bf3ef18afefad631ca70f2c25ef70", - "version" : "1.4.0" + "revision" : "b4ac92a444e79d5651930482623b9f6dc9265667", + "version" : "2.0.0" } }, { diff --git a/Package.swift b/Package.swift index a10ae7fc0..506b907ff 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( .library(name: "DDGSync", targets: ["DDGSync"]), .library(name: "Persistence", targets: ["Persistence"]), .library(name: "Bookmarks", targets: ["Bookmarks"]), + .library(name: "BloomFilterWrapper", targets: ["BloomFilterWrapper"]), .library(name: "UserScript", targets: ["UserScript"]), .library(name: "Crashes", targets: ["Crashes"]), .library(name: "ContentBlocking", targets: ["ContentBlocking"]), @@ -31,13 +32,13 @@ let package = Package( .library(name: "SecureStorage", targets: ["SecureStorage"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "8.4.2"), + .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "9.0.0"), .package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.2.0"), .package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.1"), .package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"), .package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "2.1.0"), - .package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "4.39.0"), - .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "1.4.0"), + .package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "4.40.0"), + .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "2.0.0"), .package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"), .package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"), .package(url: "https://github.com/duckduckgo/wireguard-apple", exact: "1.1.1") @@ -87,11 +88,16 @@ let package = Package( "Bookmarks" ] ), - .target( - name: "BloomFilterWrapper", + .target( + name: "BloomFilterObjC", dependencies: [ .product(name: "BloomFilter", package: "bloom_cpp") ]), + .target( + name: "BloomFilterWrapper", + dependencies: [ + "BloomFilterObjC" + ]), .target( name: "Crashes" ), @@ -196,6 +202,9 @@ let package = Package( .target(name: "WireGuardC"), .product(name: "WireGuard", package: "wireguard-apple"), "Common" + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) ]), .target( name: "SecureStorage", diff --git a/Sources/BloomFilterWrapper/BloomFilterWrapper.mm b/Sources/BloomFilterObjC/BloomFilterObjC.mm similarity index 93% rename from Sources/BloomFilterWrapper/BloomFilterWrapper.mm rename to Sources/BloomFilterObjC/BloomFilterObjC.mm index f3f3dbb74..c32bf6dc8 100644 --- a/Sources/BloomFilterWrapper/BloomFilterWrapper.mm +++ b/Sources/BloomFilterObjC/BloomFilterObjC.mm @@ -17,15 +17,15 @@ // limitations under the License. // -#import "BloomFilterWrapper.h" +#import "BloomFilterObjC.h" #import "BloomFilter.hpp" -@interface BloomFilterWrapper() { +@interface BloomFilterObjC() { BloomFilter *filter; } @end -@implementation BloomFilterWrapper +@implementation BloomFilterObjC - (instancetype)initFromPath:(NSString*)path withBitCount:(int)bitCount andTotalItems:(int)totalItems { self = [super init]; diff --git a/Sources/BloomFilterWrapper/include/BloomFilterWrapper.h b/Sources/BloomFilterObjC/include/BloomFilterObjC.h similarity index 95% rename from Sources/BloomFilterWrapper/include/BloomFilterWrapper.h rename to Sources/BloomFilterObjC/include/BloomFilterObjC.h index 87c7b8371..3be291b38 100644 --- a/Sources/BloomFilterWrapper/include/BloomFilterWrapper.h +++ b/Sources/BloomFilterObjC/include/BloomFilterObjC.h @@ -18,7 +18,7 @@ // #import -@interface BloomFilterWrapper : NSObject +@interface BloomFilterObjC: NSObject - (instancetype)initFromPath:(NSString*)path withBitCount:(int)bitCount andTotalItems:(int)totalItems; - (instancetype)initWithTotalItems:(int)count errorRate:(double)errorRate; - (void)dealloc; diff --git a/Sources/BloomFilterWrapper/include/module.modulemap b/Sources/BloomFilterObjC/include/module.private.modulemap similarity index 55% rename from Sources/BloomFilterWrapper/include/module.modulemap rename to Sources/BloomFilterObjC/include/module.private.modulemap index 4ccf806f5..e275f75d5 100644 --- a/Sources/BloomFilterWrapper/include/module.modulemap +++ b/Sources/BloomFilterObjC/include/module.private.modulemap @@ -1,4 +1,4 @@ module BloomFilterWrapper { - header "BloomFilterWrapper.h" + header "BloomFilterObjC.h" export * } diff --git a/Sources/BloomFilterWrapper/BloomFilterWrapper.swift b/Sources/BloomFilterWrapper/BloomFilterWrapper.swift new file mode 100644 index 000000000..562007b76 --- /dev/null +++ b/Sources/BloomFilterWrapper/BloomFilterWrapper.swift @@ -0,0 +1,40 @@ +// +// BloomFilterWrapper.swift +// +// Copyright © 2023 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 +@_implementationOnly import BloomFilterObjC + +public final class BloomFilterWrapper { + private let bloomFilter: BloomFilterObjC + + public init(fromPath path: String, withBitCount bitCount: Int32, andTotalItems totalItems: Int32) { + bloomFilter = BloomFilterObjC(fromPath: path, withBitCount: bitCount, andTotalItems: totalItems) + } + + public init(totalItems count: Int32, errorRate: Double) { + bloomFilter = BloomFilterObjC(totalItems: count, errorRate: errorRate) + } + + public func add(_ entry: String) { + bloomFilter.add(entry) + } + + public func contains(_ entry: String) -> Bool { + bloomFilter.contains(entry) + } +} diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesManager.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesManager.swift index 13fda8930..52c02d635 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesManager.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesManager.swift @@ -116,6 +116,7 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { public var updatesPublisher: AnyPublisher { updatesSubject.eraseToAnyPublisher() } + public var onCriticalError: (() -> Void)? private let errorReporting: EventMapping? private let getLog: () -> OSLog @@ -288,6 +289,7 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { sourceManager = ContentBlockerRulesSourceManager(rulesList: rulesList, exceptionsSource: self.exceptionsSource, errorReporting: self.errorReporting, + onCriticalError: self.onCriticalError, log: log) self.sourceManagers[rulesList.name] = sourceManager } diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSourceManager.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSourceManager.swift index 3a93bee2f..b67712c6f 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSourceManager.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSourceManager.swift @@ -96,6 +96,7 @@ public class ContentBlockerRulesSourceManager { public private(set) var fallbackTDSFailure = false private let errorReporting: EventMapping? + private let onCriticalError: (() -> Void)? private let getLog: () -> OSLog private var log: OSLog { getLog() @@ -105,10 +106,12 @@ public class ContentBlockerRulesSourceManager { init(rulesList: ContentBlockerRulesList, exceptionsSource: ContentBlockerRulesExceptionsSource, errorReporting: EventMapping? = nil, + onCriticalError: (() -> Void)? = nil, log: @escaping @autoclosure () -> OSLog = .disabled) { self.rulesList = rulesList self.exceptionsSource = exceptionsSource self.errorReporting = errorReporting + self.onCriticalError = onCriticalError self.getLog = log } @@ -186,7 +189,7 @@ public class ContentBlockerRulesSourceManager { compilationFailed(for: input, with: error, brokenSources: brokenSources) return } - + compilationFailed(for: input, with: error, brokenSources: brokenSources) } @@ -196,6 +199,7 @@ public class ContentBlockerRulesSourceManager { private func compilationFailed(for input: ContentBlockerRulesSourceIdentifiers, with error: Error, brokenSources: RulesSourceBreakageInfo) { + if input.tdsIdentifier != rulesList.fallbackTrackerData.etag { os_log(.debug, log: log, "Falling back to embedded TDS") // We failed compilation for non-embedded TDS, marking it as broken. @@ -243,10 +247,19 @@ public class ContentBlockerRulesSourceManager { parameters: params, onComplete: { _ in if input.name == DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName { - fatalError("Could not compile embedded rules list") + self.handleCriticalError() } }) fallbackTDSFailure = true } } + + private func handleCriticalError() { + if let onCriticalError = self.onCriticalError { + onCriticalError() + } else { + fatalError("Could not compile embedded rules list") + } + } + } diff --git a/Sources/BrowserServicesKit/Email/EmailManager.swift b/Sources/BrowserServicesKit/Email/EmailManager.swift index 7cc298919..a5f88c9a9 100644 --- a/Sources/BrowserServicesKit/Email/EmailManager.swift +++ b/Sources/BrowserServicesKit/Email/EmailManager.swift @@ -104,8 +104,10 @@ public protocol EmailManagerRequestDelegate: AnyObject { httpBody: Data?, timeoutInterval: TimeInterval) async throws -> Data - func emailManagerKeychainAccessFailed(accessType: EmailKeychainAccessType, error: EmailKeychainAccessError) - + func emailManagerKeychainAccessFailed(_ emailManager: EmailManager, + accessType: EmailKeychainAccessType, + error: EmailKeychainAccessError) + } // swiftlint:enable function_parameter_count @@ -164,6 +166,7 @@ public class EmailManager { public enum NotificationParameter { public static let cohort = "cohort" + public static let isForcedSignOut = "isForcedSignOut" } private lazy var emailUrls = EmailUrls() @@ -184,7 +187,7 @@ public class EmailManager { return try storage.getUsername() } catch { if let error = error as? EmailKeychainAccessError { - requestDelegate?.emailManagerKeychainAccessFailed(accessType: .getUsername, error: error) + requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .getUsername, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -203,7 +206,7 @@ public class EmailManager { return try storage.getToken() } catch { if let error = error as? EmailKeychainAccessError { - requestDelegate?.emailManagerKeychainAccessFailed(accessType: .getToken, error: error) + requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .getToken, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -222,7 +225,7 @@ public class EmailManager { return try storage.getAlias() } catch { if let error = error as? EmailKeychainAccessError { - requestDelegate?.emailManagerKeychainAccessFailed(accessType: .getAlias, error: error) + requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .getAlias, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -241,7 +244,7 @@ public class EmailManager { return try storage.getCohort() } catch { if let error = error as? EmailKeychainAccessError { - requestDelegate?.emailManagerKeychainAccessFailed(accessType: .getCohort, error: error) + requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .getCohort, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -260,7 +263,7 @@ public class EmailManager { return try storage.getLastUseDate() ?? "" } catch { if let error = error as? EmailKeychainAccessError { - requestDelegate?.emailManagerKeychainAccessFailed(accessType: .getLastUseData, error: error) + requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .getLastUseData, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -281,7 +284,7 @@ public class EmailManager { try storage.store(lastUseDate: dateString) } catch { if let error = error as? EmailKeychainAccessError { - requestDelegate?.emailManagerKeychainAccessFailed(accessType: .storeLastUseDate, error: error) + requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .storeLastUseDate, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -314,7 +317,7 @@ public class EmailManager { dateFormatter.timeZone = TimeZone(identifier: "America/New_York") // Use ET time zone } - public func signOut() throws { + public func signOut(isForced: Bool = false) throws { Self.lock.lock() defer { Self.lock.unlock() @@ -331,12 +334,13 @@ public class EmailManager { if let currentCohortValue = currentCohortValue { notificationParameters[NotificationParameter.cohort] = currentCohortValue } + notificationParameters[NotificationParameter.isForcedSignOut] = isForced ? "true" : nil NotificationCenter.default.post(name: .emailDidSignOut, object: self, userInfo: notificationParameters) } catch { if let error = error as? EmailKeychainAccessError { - self.requestDelegate?.emailManagerKeychainAccessFailed(accessType: .deleteAuthenticationState, error: error) + self.requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .deleteAuthenticationState, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -344,6 +348,10 @@ public class EmailManager { } } + public func forceSignOut() { + try? signOut(isForced: true) + } + public func emailAddressFor(_ alias: String) -> String { return alias + "@" + Self.emailDomain } @@ -540,7 +548,7 @@ public extension EmailManager { } catch { if let error = error as? EmailKeychainAccessError { - requestDelegate?.emailManagerKeychainAccessFailed(accessType: .storeTokenUsernameCohort, error: error) + requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .storeTokenUsernameCohort, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -597,7 +605,7 @@ private extension EmailManager { try storage.deleteAlias() } catch { if let error = error as? EmailKeychainAccessError { - self.requestDelegate?.emailManagerKeychainAccessFailed(accessType: .deleteAlias, error: error) + self.requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .deleteAlias, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } @@ -642,7 +650,7 @@ private extension EmailManager { try self.storage.store(alias: alias) } catch { if let error = error as? EmailKeychainAccessError { - self.requestDelegate?.emailManagerKeychainAccessFailed(accessType: .storeAlias, error: error) + self.requestDelegate?.emailManagerKeychainAccessFailed(self, accessType: .storeAlias, error: error) } else { assertionFailure("Expected EmailKeychainAccessFailure") } diff --git a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeStore.swift b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeStore.swift index 25732a529..99bdb8a07 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeStore.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeStore.swift @@ -17,6 +17,7 @@ // import BloomFilterWrapper +import Foundation public protocol HTTPSUpgradeStore { diff --git a/Sources/BrowserServicesKit/SmarterEncryption/Store/AppHTTPSUpgradeStore.swift b/Sources/BrowserServicesKit/SmarterEncryption/Store/AppHTTPSUpgradeStore.swift index 1a497611d..714f5b1f5 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/Store/AppHTTPSUpgradeStore.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/Store/AppHTTPSUpgradeStore.swift @@ -102,7 +102,7 @@ public struct AppHTTPSUpgradeStore: HTTPSUpgradeStore { let wrapper = BloomFilterWrapper(fromPath: bloomFilterDataURL.path, withBitCount: Int32(specification.bitCount), andTotalItems: Int32(specification.totalEntries)) - return BloomFilter(wrapper: wrapper!, specification: specification) + return BloomFilter(wrapper: wrapper, specification: specification) } func loadStoredBloomFilterSpecification() -> HTTPSBloomFilterSpecification? { diff --git a/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSUpgrade.xcdatamodeld/.xccurrentversion b/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSUpgrade.xcdatamodeld/.xccurrentversion index 7ae5865f4..0c67376eb 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSUpgrade.xcdatamodeld/.xccurrentversion +++ b/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSUpgrade.xcdatamodeld/.xccurrentversion @@ -1,8 +1,5 @@ - - _XCCurrentVersionName - HTTPSUpgrade 3.xcdatamodel - + diff --git a/Sources/Crashes/CrashCollection.swift b/Sources/Crashes/CrashCollection.swift index 8a32db66c..9fa65323b 100644 --- a/Sources/Crashes/CrashCollection.swift +++ b/Sources/Crashes/CrashCollection.swift @@ -41,7 +41,7 @@ public struct CrashCollection { .flatMap { $0 } .forEach { completion([ - "appVersion": $0.applicationVersion, + "appVersion": "\($0.applicationVersion).\($0.metaData.applicationBuildVersion)", "code": "\($0.exceptionCode ?? -1)", "type": "\($0.exceptionType ?? -1)", "signal": "\($0.signal ?? -1)" diff --git a/Sources/NetworkProtection/Controllers/TunnelController.swift b/Sources/NetworkProtection/Controllers/TunnelController.swift index d41e873f3..d95f1348c 100644 --- a/Sources/NetworkProtection/Controllers/TunnelController.swift +++ b/Sources/NetworkProtection/Controllers/TunnelController.swift @@ -31,4 +31,8 @@ public protocol TunnelController { /// Stops the VPN connection used for Network Protection /// func stop() async + + /// Whether the tunnel is connected + /// + var isConnected: Bool { get async } } diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift index e749efff5..57b478cfa 100644 --- a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift @@ -22,6 +22,11 @@ public enum ExtensionMessage: RawRepresentable { public typealias RawValue = Data enum Name: UInt8 { + // This is actually an improved way to send messages. + // Please avoid adding new messages to this enum, and instead + // add them to `ExtensionRequest` + case request = 255 + case resetAllState = 0 case getRuntimeConfiguration case getLastErrorMessage @@ -40,6 +45,11 @@ public enum ExtensionMessage: RawRepresentable { case simulateConnectionInterruption } + // This is actually an improved way to send messages. + // Please avoid adding new messages to this enum, and instead + // add them to `ExtensionRequest` + case request(_ request: ExtensionRequest) + // important: Preserve this order because Message Name is represented by Int value case resetAllState case getRuntimeConfiguration @@ -62,6 +72,12 @@ public enum ExtensionMessage: RawRepresentable { public init?(rawValue data: Data) { let name = data.first.flatMap(Name.init(rawValue:)) switch name { + case .request: + guard let request = try? JSONDecoder().decode(ExtensionRequest.self, from: data[1...]) else { + return nil + } + + self = .request(request) case .resetAllState: self = .resetAllState case .getRuntimeConfiguration: @@ -127,6 +143,7 @@ public enum ExtensionMessage: RawRepresentable { // TO BE: Replaced with auto case name generating Macro when Xcode 15 private var name: Name { switch self { + case .request: return .request case .resetAllState: return .resetAllState case .getRuntimeConfiguration: return .getRuntimeConfiguration case .getLastErrorMessage: return .getLastErrorMessage @@ -149,6 +166,14 @@ public enum ExtensionMessage: RawRepresentable { public var rawValue: Data { var encoder: (inout Data) -> Void = { _ in } switch self { + case .request(let request): + encoder = { + do { + try $0.append(JSONEncoder().encode(request)) + } catch { + assertionFailure("could not encode request: \(error)") + } + } case .setSelectedServer(.some(let serverName)): encoder = { $0.append(ExtensionMessageString(serverName).rawValue) diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift new file mode 100644 index 000000000..06f016f83 --- /dev/null +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift @@ -0,0 +1,29 @@ +// +// ExtensionRequest.swift +// +// Copyright © 2023 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 + +public enum DebugCommand: Codable { + case expireRegistrationKey + case sendTestNotification +} + +public enum ExtensionRequest: Codable { + case changeTunnelSetting(_ change: TunnelSettings.Change) + case debugCommand(_ command: DebugCommand) +} diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 49e7b5976..0a7006ad8 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -111,9 +111,11 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { return self.protocolConfiguration.enforceRoutes || self.protocolConfiguration.includeAllNetworks } - // MARK: - Server Selection + // MARK: - Tunnel Settings + + private let settings = TunnelSettings(defaults: .standard) - let selectedServerStore = NetworkProtectionSelectedServerUserDefaultsStore() + // MARK: - Server Selection public var lastSelectedServerInfo: NetworkProtectionServerInfo? { didSet { @@ -137,25 +139,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { private let tokenStore: NetworkProtectionTokenStore - /// This is for overriding the defaults. A `nil` value means NetP will just use the defaults. - /// - private var keyValidity: TimeInterval? - - private static let defaultRetryInterval: TimeInterval = .minutes(1) - - /// Normally we'll retry using the default interval, but since we can override the key validity interval for testing purposes - /// we'll retry sooner if it's been overridden with values lower than the default retry interval. - /// - /// In practical terms this means that if the validity interval is 15 secs, the retry will also be 15 secs instead of 1 minute. - /// - private var retryInterval: TimeInterval { - guard let keyValidity = keyValidity else { - return Self.defaultRetryInterval - } - - return keyValidity > Self.defaultRetryInterval ? Self.defaultRetryInterval : keyValidity - } - private func resetRegistrationKey() { os_log("Resetting the current registration key", log: .networkProtectionKeyManagement) keyStore.resetCurrentKeyPair() @@ -182,17 +165,13 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { self.resetRegistrationKey() do { - try await updateTunnelConfiguration(selectedServer: selectedServerStore.selectedServer, reassert: false) + try await updateTunnelConfiguration(selectedServer: settings.selectedServer, reassert: false) } catch { os_log("Rekey attempt failed. This is not an error if you're using debug Key Management options: %{public}@", log: .networkProtectionKeyManagement, type: .error, String(describing: error)) } } private func setKeyValidity(_ interval: TimeInterval?) { - guard keyValidity != interval else { - return - } - if let interval { let firstExpirationDate = Date().addingTimeInterval(interval) @@ -200,9 +179,13 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { log: .networkProtectionKeyManagement, String(describing: interval), String(describing: firstExpirationDate)) + + settings.registrationKeyValidity = .custom(interval) } else { os_log("Resetting key validity interval", log: .networkProtectionKeyManagement) + + settings.registrationKeyValidity = .automatic } keyStore.setValidityInterval(interval) @@ -387,11 +370,11 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { private func loadSelectedServer(from options: StartupOptions) { switch options.selectedServer { case .set(let selectedServer): - selectedServerStore.selectedServer = selectedServer + settings.selectedServer = selectedServer case .useExisting: break case .reset: - selectedServerStore.selectedServer = .automatic + settings.selectedServer = .automatic } } @@ -491,15 +474,15 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { let onDemand = options.startupMethod == .automaticOnDemand os_log("Starting tunnel %{public}@", log: .networkProtection, options.startupMethod.debugDescription) - startTunnel(selectedServer: selectedServerStore.selectedServer, onDemand: onDemand, completionHandler: completionHandler) + startTunnel(selectedServer: settings.selectedServer, onDemand: onDemand, completionHandler: completionHandler) } - private func startTunnel(selectedServer: SelectedNetworkProtectionServer, onDemand: Bool, completionHandler: @escaping (Error?) -> Void) { + private func startTunnel(selectedServer: TunnelSettings.SelectedServer, onDemand: Bool, completionHandler: @escaping (Error?) -> Void) { Task { let serverSelectionMethod: NetworkProtectionServerSelectionMethod - switch selectedServerStore.selectedServer { + switch settings.selectedServer { case .automatic: serverSelectionMethod = .automatic case .endpoint(let serverName): @@ -633,10 +616,10 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { // MARK: - Tunnel Configuration - public func updateTunnelConfiguration(selectedServer: SelectedNetworkProtectionServer, reassert: Bool = true) async throws { + public func updateTunnelConfiguration(selectedServer: TunnelSettings.SelectedServer, reassert: Bool = true) async throws { let serverSelectionMethod: NetworkProtectionServerSelectionMethod - switch selectedServerStore.selectedServer { + switch settings.selectedServer { case .automatic: serverSelectionMethod = .automatic case .endpoint(let serverName): @@ -717,6 +700,8 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } switch message { + case .request(let request): + handleRequest(request) case .expireRegistrationKey: handleExpireRegistrationKey(completionHandler: completionHandler) case .getLastErrorMessage: @@ -736,7 +721,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { case .resetAllState: handleResetAllState(completionHandler: completionHandler) case .triggerTestNotification: - handleTriggerTestNotification(completionHandler: completionHandler) + handleSendTestNotification(completionHandler: completionHandler) case .setExcludedRoutes(let excludedRoutes): setExcludedRoutes(excludedRoutes, completionHandler: completionHandler) case .setIncludedRoutes(let includedRoutes): @@ -752,6 +737,54 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } } + // MARK: - App Requests: Handling + + private func handleRequest(_ request: ExtensionRequest, completionHandler: ((Data?) -> Void)? = nil) { + switch request { + case .changeTunnelSetting(let change): + handleSettingsChange(change, completionHandler: completionHandler) + case .debugCommand(let command): + handleDebugCommand(command, completionHandler: completionHandler) + } + } + + private func handleSettingsChange(_ change: TunnelSettings.Change, completionHandler: ((Data?) -> Void)? = nil) { + + settings.apply(change: change) + + switch change { + case .setSelectedServer(let selectedServer): + let serverSelectionMethod: NetworkProtectionServerSelectionMethod + + switch selectedServer { + case .automatic: + serverSelectionMethod = .automatic + case .endpoint(let serverName): + serverSelectionMethod = .preferredServer(serverName: serverName) + } + + Task { + try? await updateTunnelConfiguration(serverSelectionMethod: serverSelectionMethod) + completionHandler?(nil) + } + case .setIncludeAllNetworks, + .setEnforceRoutes, + .setExcludeLocalNetworks, + .setRegistrationKeyValidity: + // Intentional no-op, as some setting changes don't require any further operation + break + } + } + + private func handleDebugCommand(_ command: DebugCommand, completionHandler: ((Data?) -> Void)? = nil) { + switch command { + case .expireRegistrationKey: + handleExpireRegistrationKey(completionHandler: completionHandler) + case .sendTestNotification: + handleSendTestNotification(completionHandler: completionHandler) + } + } + // MARK: - App Messages: Handling private func handleExpireRegistrationKey(completionHandler: ((Data?) -> Void)? = nil) { @@ -794,20 +827,20 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { private func handleSetSelectedServer(_ serverName: String?, completionHandler: ((Data?) -> Void)? = nil) { Task { guard let serverName else { - if case .endpoint = selectedServerStore.selectedServer { - selectedServerStore.selectedServer = .automatic + if case .endpoint = settings.selectedServer { + settings.selectedServer = .automatic try? await updateTunnelConfiguration(serverSelectionMethod: .automatic) } completionHandler?(nil) return } - guard selectedServerStore.selectedServer.stringValue != serverName else { + guard settings.selectedServer.stringValue != serverName else { completionHandler?(nil) return } - selectedServerStore.selectedServer = .endpoint(serverName) + settings.selectedServer = .endpoint(serverName) try? await updateTunnelConfiguration(serverSelectionMethod: .preferredServer(serverName: serverName)) completionHandler?(nil) } @@ -830,7 +863,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } } - private func handleTriggerTestNotification(completionHandler: ((Data?) -> Void)? = nil) { + private func handleSendTestNotification(completionHandler: ((Data?) -> Void)? = nil) { notificationsPresenter.showTestNotification() completionHandler?(nil) } @@ -838,7 +871,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { private func setExcludedRoutes(_ excludedRoutes: [IPAddressRange], completionHandler: ((Data?) -> Void)? = nil) { Task { self.excludedRoutes = excludedRoutes - try? await updateTunnelConfiguration(selectedServer: selectedServerStore.selectedServer, reassert: false) + try? await updateTunnelConfiguration(selectedServer: settings.selectedServer, reassert: false) completionHandler?(nil) } } @@ -846,7 +879,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { private func setIncludedRoutes(_ includedRoutes: [IPAddressRange], completionHandler: ((Data?) -> Void)? = nil) { Task { self.includedRoutes = includedRoutes - try? await updateTunnelConfiguration(selectedServer: selectedServerStore.selectedServer, reassert: false) + try? await updateTunnelConfiguration(selectedServer: settings.selectedServer, reassert: false) completionHandler?(nil) } } diff --git a/Sources/NetworkProtection/Session/ConnectionSessionUtilities.swift b/Sources/NetworkProtection/Session/ConnectionSessionUtilities.swift index d446c81dd..951c6a2bb 100644 --- a/Sources/NetworkProtection/Session/ConnectionSessionUtilities.swift +++ b/Sources/NetworkProtection/Session/ConnectionSessionUtilities.swift @@ -22,6 +22,24 @@ import NetworkExtension /// These are only usable from the App that owns the tunnel. /// public class ConnectionSessionUtilities { + public static func activeSession(networkExtensionBundleID: String) async throws -> NETunnelProviderSession? { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + + guard let manager = managers.first(where: { + ($0.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == networkExtensionBundleID + }) else { + // No active connection, this is acceptable + return nil + } + + guard let session = manager.connection as? NETunnelProviderSession else { + // The active connection is not running, so there's no session, this is acceptable + return nil + } + + return session + } + public static func activeSession() async throws -> NETunnelProviderSession? { let managers = try await NETunnelProviderManager.loadAllFromPreferences() @@ -53,6 +71,13 @@ public extension NETunnelProviderSession { // MARK: - ExtensionMessage + func sendProviderMessage(_ message: ExtensionMessage, + responseHandler: @escaping () -> Void) throws { + try sendProviderMessage(message.rawValue) { _ in + responseHandler() + } + } + func sendProviderMessage(_ message: ExtensionMessage, responseHandler: @escaping (T?) -> Void) throws where T.RawValue == Data { try sendProviderMessage(message.rawValue) { response in @@ -60,11 +85,20 @@ public extension NETunnelProviderSession { } } - func sendProviderMessage(_ message: ExtensionMessage) async throws -> T? where T.RawValue == Data { + func sendProviderRequest(_ request: ExtensionRequest) async throws { + try await sendProviderMessage(.request(request)) + } + + func sendProviderRequest(_ request: ExtensionRequest) async throws -> T? where T.RawValue == Data { + + try await sendProviderMessage(.request(request)) + } + + func sendProviderMessage(_ message: ExtensionMessage) async throws { try await withCheckedThrowingContinuation { continuation in do { - try sendProviderMessage(message) { response in - continuation.resume(returning: response) + try sendProviderMessage(message) { + continuation.resume() } } catch { continuation.resume(throwing: error) @@ -72,9 +106,15 @@ public extension NETunnelProviderSession { } } - func sendProviderMessage(_ message: ExtensionMessage, completionHandler: (() -> Void)? = nil) throws { - try sendProviderMessage(message.rawValue) { _ in - completionHandler?() + func sendProviderMessage(_ message: ExtensionMessage) async throws -> T? where T.RawValue == Data { + try await withCheckedThrowingContinuation { continuation in + do { + try sendProviderMessage(message) { response in + continuation.resume(returning: response) + } + } catch { + continuation.resume(throwing: error) + } } } } diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+enforceRoutes.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+enforceRoutes.swift new file mode 100644 index 000000000..553f8625a --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+enforceRoutes.swift @@ -0,0 +1,45 @@ +// +// UserDefaults+enforceRoutes.swift +// +// Copyright © 2023 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 Combine +import Foundation + +extension UserDefaults { + private var enforceRoutesKey: String { + "networkProtectionSettingEnforceRoutes" + } + + @objc + dynamic var networkProtectionSettingEnforceRoutes: Bool { + get { + bool(forKey: enforceRoutesKey) + } + + set { + set(newValue, forKey: enforceRoutesKey) + } + } + + var networkProtectionSettingEnforceRoutesPublisher: AnyPublisher { + publisher(for: \.networkProtectionSettingEnforceRoutes).eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingEnforceRoutes() { + removeObject(forKey: enforceRoutesKey) + } +} diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+excludeLocalNetworks.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+excludeLocalNetworks.swift new file mode 100644 index 000000000..75df3458d --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+excludeLocalNetworks.swift @@ -0,0 +1,45 @@ +// +// UserDefaults+excludeLocalNetworks.swift +// +// Copyright © 2023 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 Combine +import Foundation + +extension UserDefaults { + private var excludeLocalNetworksKey: String { + "networkProtectionSettingExcludeLocalNetworks" + } + + @objc + dynamic var networkProtectionSettingExcludeLocalNetworks: Bool { + get { + bool(forKey: excludeLocalNetworksKey) + } + + set { + set(newValue, forKey: excludeLocalNetworksKey) + } + } + + var networkProtectionSettingExcludeLocalNetworksPublisher: AnyPublisher { + publisher(for: \.networkProtectionSettingExcludeLocalNetworks).eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingExcludeLocalNetworks() { + removeObject(forKey: excludeLocalNetworksKey) + } +} diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+includeAllNetworks.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+includeAllNetworks.swift new file mode 100644 index 000000000..4178af108 --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+includeAllNetworks.swift @@ -0,0 +1,45 @@ +// +// UserDefaults+includeAllNetworks.swift +// +// Copyright © 2023 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 Combine +import Foundation + +extension UserDefaults { + private var includeAllNetworksKey: String { + "networkProtectionSettingIncludeAllNetworks" + } + + @objc + dynamic var networkProtectionSettingIncludeAllNetworks: Bool { + get { + bool(forKey: includeAllNetworksKey) + } + + set { + set(newValue, forKey: includeAllNetworksKey) + } + } + + var networkProtectionSettingIncludeAllNetworksPublisher: AnyPublisher { + publisher(for: \.networkProtectionSettingIncludeAllNetworks).eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingIncludeAllNetworks() { + removeObject(forKey: includeAllNetworksKey) + } +} diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+registrationKeyValidity.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+registrationKeyValidity.swift new file mode 100644 index 000000000..63ca7ddc8 --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+registrationKeyValidity.swift @@ -0,0 +1,72 @@ +// +// UserDefaults+registrationKeyValidity.swift +// +// Copyright © 2023 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 Combine +import Foundation + +extension UserDefaults { + private var registrationKeyValidityKey: String { + "networkProtectionSettingRegistrationKeyValidityRawValue" + } + + @objc + dynamic var networkProtectionSettingRegistrationKeyValidityRawValue: NSNumber? { + get { + value(forKey: registrationKeyValidityKey) as? NSNumber + } + + set { + set(newValue, forKey: registrationKeyValidityKey) + } + } + + private func registrationKeyValidityFromRawValue(_ rawValue: NSNumber?) -> TunnelSettings.RegistrationKeyValidity { + guard let timeInterval = networkProtectionSettingRegistrationKeyValidityRawValue?.doubleValue else { + return .automatic + } + + return .custom(timeInterval) + } + + var networkProtectionSettingRegistrationKeyValidity: TunnelSettings.RegistrationKeyValidity { + get { + registrationKeyValidityFromRawValue(networkProtectionSettingRegistrationKeyValidityRawValue) + } + + set { + switch newValue { + case .automatic: + networkProtectionSettingRegistrationKeyValidityRawValue = nil + case .custom(let timeInterval): + networkProtectionSettingRegistrationKeyValidityRawValue = NSNumber(value: timeInterval) + } + } + } + + var networkProtectionSettingRegistrationKeyValidityPublisher: AnyPublisher { + let registrationKeyValidityFromRawValue = self.registrationKeyValidityFromRawValue + + return publisher(for: \.networkProtectionSettingRegistrationKeyValidityRawValue).map { serverName in + registrationKeyValidityFromRawValue(serverName) + }.eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingRegistrationKeyValidity() { + networkProtectionSettingRegistrationKeyValidityRawValue = nil + } +} diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedServer.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedServer.swift new file mode 100644 index 000000000..4c2ad0246 --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedServer.swift @@ -0,0 +1,72 @@ +// +// UserDefaults+selectedServer.swift +// +// Copyright © 2023 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 Combine +import Foundation + +extension UserDefaults { + private var selectedServerKey: String { + "networkProtectionSettingSelectedServerRawValue" + } + + @objc + dynamic var networkProtectionSettingSelectedServerRawValue: String? { + get { + value(forKey: selectedServerKey) as? String + } + + set { + set(newValue, forKey: selectedServerKey) + } + } + + private func selectedServerFromRawValue(_ rawValue: String?) -> TunnelSettings.SelectedServer { + guard let selectedEndpoint = networkProtectionSettingSelectedServerRawValue else { + return .automatic + } + + return .endpoint(selectedEndpoint) + } + + var networkProtectionSettingSelectedServer: TunnelSettings.SelectedServer { + get { + selectedServerFromRawValue(networkProtectionSettingSelectedServerRawValue) + } + + set { + switch newValue { + case .automatic: + networkProtectionSettingSelectedServerRawValue = nil + case .endpoint(let serverName): + networkProtectionSettingSelectedServerRawValue = serverName + } + } + } + + var networkProtectionSettingSelectedServerPublisher: AnyPublisher { + let selectedServerFromRawValue = self.selectedServerFromRawValue + + return publisher(for: \.networkProtectionSettingSelectedServerRawValue).map { serverName in + selectedServerFromRawValue(serverName) + }.eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingSelectedServer() { + networkProtectionSettingSelectedServerRawValue = nil + } +} diff --git a/Sources/NetworkProtection/Settings/TunnelSettings.swift b/Sources/NetworkProtection/Settings/TunnelSettings.swift new file mode 100644 index 000000000..69871318f --- /dev/null +++ b/Sources/NetworkProtection/Settings/TunnelSettings.swift @@ -0,0 +1,230 @@ +// +// TunnelSettings.swift +// +// Copyright © 2023 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 Combine +import Foundation + +/// Persists and publishes changes to tunnel settings. +/// +/// It's strongly recommended to use shared `UserDefaults` to initialize this class, as `TunnelSettingsUpdater` +/// can then detect settings changes using KVO even if they're applied by a different process or even by the user through +/// the command line. +/// +public final class TunnelSettings { + + public enum Change: Codable { + case setIncludeAllNetworks(_ includeAllNetworks: Bool) + case setEnforceRoutes(_ enforceRoutes: Bool) + case setExcludeLocalNetworks(_ excludeLocalNetworks: Bool) + case setRegistrationKeyValidity(_ validity: RegistrationKeyValidity) + case setSelectedServer(_ selectedServer: SelectedServer) + } + + public enum RegistrationKeyValidity: Codable { + case automatic + case custom(_ timeInterval: TimeInterval) + } + + public enum SelectedServer: Codable, Equatable { + case automatic + case endpoint(String) + + public var stringValue: String? { + switch self { + case .automatic: return nil + case .endpoint(let endpoint): return endpoint + } + } + } + + private let defaults: UserDefaults + + private(set) public lazy var changePublisher: AnyPublisher = { + + let includeAllNetworksPublisher = includeAllNetworksPublisher.map { includeAllNetworks in + Change.setIncludeAllNetworks(includeAllNetworks) + }.eraseToAnyPublisher() + + let enforceRoutesPublisher = enforceRoutesPublisher.map { enforceRoutes in + Change.setEnforceRoutes(enforceRoutes) + }.eraseToAnyPublisher() + + let excludeLocalNetworksPublisher = excludeLocalNetworksPublisher.map { excludeLocalNetworks in + Change.setExcludeLocalNetworks(excludeLocalNetworks) + }.eraseToAnyPublisher() + + let registrationKeyValidityPublisher = registrationKeyValidityPublisher.map { validity in + Change.setRegistrationKeyValidity(validity) + }.eraseToAnyPublisher() + + let serverChangePublisher = selectedServerPublisher.map { server in + Change.setSelectedServer(server) + }.eraseToAnyPublisher() + + return Publishers.MergeMany( + includeAllNetworksPublisher, + enforceRoutesPublisher, + excludeLocalNetworksPublisher, + serverChangePublisher).eraseToAnyPublisher() + }() + + public init(defaults: UserDefaults) { + self.defaults = defaults + } + + // MARK: - Resetting to Defaults + + public func resetToDefaults() { + defaults.resetNetworkProtectionSettingEnforceRoutes() + defaults.resetNetworkProtectionSettingExcludeLocalNetworks() + defaults.resetNetworkProtectionSettingIncludeAllNetworks() + defaults.resetNetworkProtectionSettingRegistrationKeyValidity() + defaults.resetNetworkProtectionSettingSelectedServer() + } + + // MARK: - Applying Changes + + public func apply(change: Change) { + switch change { + case .setEnforceRoutes(let enforceRoutes): + self.enforceRoutes = enforceRoutes + case .setExcludeLocalNetworks(let excludeLocalNetworks): + self.excludeLocalNetworks = excludeLocalNetworks + case .setIncludeAllNetworks(let includeAllNetworks): + self.includeAllNetworks = includeAllNetworks + case .setRegistrationKeyValidity(let registrationKeyValidity): + self.registrationKeyValidity = registrationKeyValidity + case .setSelectedServer(let selectedServer): + self.selectedServer = selectedServer + } + } + + // MARK: - Enforce Routes + + public var includeAllNetworksPublisher: AnyPublisher { + defaults.networkProtectionSettingIncludeAllNetworksPublisher + } + + public var includeAllNetworks: Bool { + get { + defaults.networkProtectionSettingIncludeAllNetworks + } + + set { + defaults.networkProtectionSettingIncludeAllNetworks = newValue + } + } + + // MARK: - Enforce Routes + + public var enforceRoutesPublisher: AnyPublisher { + defaults.networkProtectionSettingEnforceRoutesPublisher + } + + public var enforceRoutes: Bool { + get { + defaults.networkProtectionSettingEnforceRoutes + } + + set { + defaults.networkProtectionSettingEnforceRoutes = newValue + } + } + + // MARK: - Exclude Local Routes + + public var excludeLocalNetworksPublisher: AnyPublisher { + defaults.networkProtectionSettingExcludeLocalNetworksPublisher + } + + public var excludeLocalNetworks: Bool { + get { + defaults.networkProtectionSettingExcludeLocalNetworks + } + + set { + defaults.networkProtectionSettingExcludeLocalNetworks = newValue + } + } + + // MARK: - Registration Key Validity + + public var registrationKeyValidityPublisher: AnyPublisher { + defaults.networkProtectionSettingRegistrationKeyValidityPublisher + } + + public var registrationKeyValidity: RegistrationKeyValidity { + get { + defaults.networkProtectionSettingRegistrationKeyValidity + } + + set { + defaults.networkProtectionSettingRegistrationKeyValidity = newValue + } + } + + private var networkProtectionSettingRegistrationKeyValidityDefault: TimeInterval { + .days(2) + } + + // MARK: - Server Selection + + public var selectedServerPublisher: AnyPublisher { + defaults.networkProtectionSettingSelectedServerPublisher + } + + public var selectedServer: SelectedServer { + get { + defaults.networkProtectionSettingSelectedServer + } + + set { + defaults.networkProtectionSettingSelectedServer = newValue + } + } + + // MARK: - Routes + + public enum ExclusionListItem { + case section(String) + case exclusion(range: NetworkProtection.IPAddressRange, description: String? = nil, `default`: Bool) + } + + public let exclusionList: [ExclusionListItem] = [ + .section("IPv4 Local Routes"), + + .exclusion(range: "10.0.0.0/8" /* 255.0.0.0 */, description: "disabled for enforceRoutes", default: true), + .exclusion(range: "172.16.0.0/12" /* 255.240.0.0 */, default: true), + .exclusion(range: "192.168.0.0/16" /* 255.255.0.0 */, default: true), + .exclusion(range: "169.254.0.0/16" /* 255.255.0.0 */, description: "Link-local", default: true), + .exclusion(range: "127.0.0.0/8" /* 255.0.0.0 */, description: "Loopback", default: true), + .exclusion(range: "224.0.0.0/4" /* 240.0.0.0 (corrected subnet mask) */, description: "Multicast", default: true), + .exclusion(range: "100.64.0.0/16" /* 255.255.0.0 */, description: "Shared Address Space", default: true), + + .section("IPv6 Local Routes"), + .exclusion(range: "fe80::/10", description: "link local", default: false), + .exclusion(range: "ff00::/8", description: "multicast", default: false), + .exclusion(range: "fc00::/7", description: "local unicast", default: false), + .exclusion(range: "::1/128", description: "loopback", default: false), + + .section("duckduckgo.com"), + .exclusion(range: "52.142.124.215/32", default: false), + .exclusion(range: "52.250.42.157/32", default: false), + .exclusion(range: "40.114.177.156/32", default: false), + ] +} diff --git a/Sources/NetworkProtection/StartupOptions.swift b/Sources/NetworkProtection/StartupOptions.swift index dff73aa0e..aa8cff895 100644 --- a/Sources/NetworkProtection/StartupOptions.swift +++ b/Sources/NetworkProtection/StartupOptions.swift @@ -94,7 +94,7 @@ struct StartupOptions { let simulateCrash: Bool let simulateMemoryCrash: Bool let keyValidity: StoredOption - let selectedServer: StoredOption + let selectedServer: StoredOption let authToken: StoredOption let enableTester: StoredOption @@ -150,7 +150,7 @@ struct StartupOptions { } } - private static func readSelectedServer(from options: [String: Any], resetIfNil: Bool) -> StoredOption { + private static func readSelectedServer(from options: [String: Any], resetIfNil: Bool) -> StoredOption { StoredOption(resetIfNil: resetIfNil) { guard let serverName = options[NetworkProtectionOptionKey.selectedServer] as? String else { diff --git a/Sources/NetworkProtection/Status/NetworkProtectionNotificationsPresenter.swift b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenter.swift similarity index 100% rename from Sources/NetworkProtection/Status/NetworkProtectionNotificationsPresenter.swift rename to Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenter.swift diff --git a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift new file mode 100644 index 000000000..4e3bff243 --- /dev/null +++ b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift @@ -0,0 +1,65 @@ +// +// NetworkProtectionNotificationsPresenterTogglableDecorator.swift +// DuckDuckGo +// +// Copyright © 2023 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 public class NetworkProtectionNotificationsPresenterTogglableDecorator: NetworkProtectionNotificationsPresenter { + private let notificationSettingsStore: NetworkProtectionNotificationsSettingsStore + private let wrappeePresenter: NetworkProtectionNotificationsPresenter + + public init(notificationSettingsStore: NetworkProtectionNotificationsSettingsStore, wrappee: NetworkProtectionNotificationsPresenter) { + self.notificationSettingsStore = notificationSettingsStore + self.wrappeePresenter = wrappee + } + + public func showConnectedNotification(serverLocation: String?) { + guard notificationSettingsStore.alertsEnabled else { + return + } + wrappeePresenter.showConnectedNotification(serverLocation: serverLocation) + } + + public func showReconnectingNotification() { + guard notificationSettingsStore.alertsEnabled else { + return + } + wrappeePresenter.showReconnectingNotification() + } + + public func showConnectionFailureNotification() { + guard notificationSettingsStore.alertsEnabled else { + return + } + wrappeePresenter.showConnectionFailureNotification() + } + + public func showSupersededNotification() { + guard notificationSettingsStore.alertsEnabled else { + return + } + wrappeePresenter.showSupersededNotification() + } + + public func showTestNotification() { + guard notificationSettingsStore.alertsEnabled else { + return + } + wrappeePresenter.showTestNotification() + } +} diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionNotificationsSettingsStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionNotificationsSettingsStore.swift new file mode 100644 index 000000000..2e4384fed --- /dev/null +++ b/Sources/NetworkProtection/Storage/NetworkProtectionNotificationsSettingsStore.swift @@ -0,0 +1,48 @@ +// +// NetworkProtectionNotificationsSettingsStore.swift +// DuckDuckGo +// +// Copyright © 2023 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 + +public protocol NetworkProtectionNotificationsSettingsStore { + var alertsEnabled: Bool { get set } +} + +final public class NetworkProtectionNotificationsSettingsUserDefaultsStore: NetworkProtectionNotificationsSettingsStore { + private enum Key { + static let alerts = "com.duckduckgo.vpnNotificationSettings.alertsEnabled" + } + + private let userDefaults: UserDefaults + + public init(userDefaults: UserDefaults) { + self.userDefaults = userDefaults + } + + public var alertsEnabled: Bool { + get { + guard self.userDefaults.object(forKey: Key.alerts) != nil else { + return true + } + return self.userDefaults.bool(forKey: Key.alerts) + } + set { + self.userDefaults.set(newValue, forKey: Key.alerts) + } + } +} diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionSelectedServerStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionSelectedServerStore.swift index 2f6da4f8b..d93197465 100644 --- a/Sources/NetworkProtection/Storage/NetworkProtectionSelectedServerStore.swift +++ b/Sources/NetworkProtection/Storage/NetworkProtectionSelectedServerStore.swift @@ -17,25 +17,13 @@ // import Foundation - +/* protocol NetworkProtectionSelectedServerStore: AnyObject { - + var selectedServer: SelectedNetworkProtectionServer { get set } func reset() } -public enum SelectedNetworkProtectionServer: Equatable { - case automatic - case endpoint(String) - - public var stringValue: String? { - switch self { - case .automatic: return nil - case .endpoint(let endpoint): return endpoint - } - } -} - public final class NetworkProtectionSelectedServerUserDefaultsStore: NetworkProtectionSelectedServerStore { private enum Constants { @@ -77,4 +65,4 @@ public final class NetworkProtectionSelectedServerUserDefaultsStore: NetworkProt userDefaults.removeObject(forKey: Constants.selectedServerKey) } -} +}*/ diff --git a/Sources/NetworkProtectionTestUtils/Controllers/MockTunnelController.swift b/Sources/NetworkProtectionTestUtils/Controllers/MockTunnelController.swift index 5ca2b42cc..bb36af2a2 100644 --- a/Sources/NetworkProtectionTestUtils/Controllers/MockTunnelController.swift +++ b/Sources/NetworkProtectionTestUtils/Controllers/MockTunnelController.swift @@ -32,4 +32,8 @@ public final class MockTunnelController: TunnelController { public func stop() async { didCallStop = true } + + public var isConnected: Bool { + true + } } diff --git a/Sources/NetworkProtectionTestUtils/Storage/MockNetworkProtectionNotificationsSettingsStore.swift b/Sources/NetworkProtectionTestUtils/Storage/MockNetworkProtectionNotificationsSettingsStore.swift new file mode 100644 index 000000000..dcaf3adf3 --- /dev/null +++ b/Sources/NetworkProtectionTestUtils/Storage/MockNetworkProtectionNotificationsSettingsStore.swift @@ -0,0 +1,25 @@ +// +// MockNetworkProtectionNotificationsSettingsStore.swift +// DuckDuckGo +// +// Copyright © 2023 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 NetworkProtection + +public class MockNetworkProtectionNotificationsSettingsStore: NetworkProtectionNotificationsSettingsStore { + public var alertsEnabled: Bool = false +} diff --git a/Sources/PrivacyDashboard/PrivacyDashboardController.swift b/Sources/PrivacyDashboard/PrivacyDashboardController.swift index aefd7452d..085302444 100644 --- a/Sources/PrivacyDashboard/PrivacyDashboardController.swift +++ b/Sources/PrivacyDashboard/PrivacyDashboardController.swift @@ -27,7 +27,8 @@ public enum PrivacyDashboardOpenSettingsTarget: String { } public protocol PrivacyDashboardControllerDelegate: AnyObject { - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didChangeProtectionSwitch isEnabled: Bool) + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + didChangeProtectionSwitch protectionState: ProtectionState) func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestOpenUrlInNewTab url: URL) func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestOpenSettings target: PrivacyDashboardOpenSettingsTarget) @@ -239,8 +240,8 @@ extension PrivacyDashboardController: PrivacyDashboardUserScriptDelegate { delegate?.privacyDashboardController(self, didRequestOpenSettings: settingsTarget) } - func userScript(_ userScript: PrivacyDashboardUserScript, didChangeProtectionStateTo isProtected: Bool) { - delegate?.privacyDashboardController(self, didChangeProtectionSwitch: isProtected) + func userScript(_ userScript: PrivacyDashboardUserScript, didChangeProtectionState protectionState: ProtectionState) { + delegate?.privacyDashboardController(self, didChangeProtectionSwitch: protectionState) } func userScript(_ userScript: PrivacyDashboardUserScript, didRequestOpenUrlInNewTab url: URL) { diff --git a/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift b/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift index d062bf373..77b9c5c5e 100644 --- a/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift +++ b/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift @@ -20,9 +20,10 @@ import Foundation import WebKit import TrackerRadarKit import UserScript +import Common protocol PrivacyDashboardUserScriptDelegate: AnyObject { - func userScript(_ userScript: PrivacyDashboardUserScript, didChangeProtectionStateTo protectionState: Bool) + func userScript(_ userScript: PrivacyDashboardUserScript, didChangeProtectionState protectionState: ProtectionState) func userScript(_ userScript: PrivacyDashboardUserScript, setHeight height: Int) func userScriptDidRequestClosing(_ userScript: PrivacyDashboardUserScript) func userScriptDidRequestShowReportBrokenSite(_ userScript: PrivacyDashboardUserScript) @@ -38,6 +39,20 @@ public enum PrivacyDashboardTheme: String, Encodable { case dark } +public struct ProtectionState: Decodable { + public let isProtected: Bool + public let eventOrigin: EventOrigin + + public struct EventOrigin: Decodable { + public let screen: EventOriginScreen + } + + public enum EventOriginScreen: String, Decodable { + case primaryScreen + case breakageForm + } +} + final class PrivacyDashboardUserScript: NSObject, StaticUserScript { enum MessageNames: String, CaseIterable { @@ -91,12 +106,13 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { // MARK: - JS message handlers private func handleSetProtection(message: WKScriptMessage) { - guard let isProtected = message.body as? Bool else { - assertionFailure("privacyDashboardSetProtection: expected Bool") + + guard let protectionState: ProtectionState = DecodableHelper.decode(from: message.messageBody) else { + assertionFailure("privacyDashboardSetProtection: expected ProtectionState") return } - delegate?.userScript(self, didChangeProtectionStateTo: isProtected) + delegate?.userScript(self, didChangeProtectionState: protectionState) } private func handleSetSize(message: WKScriptMessage) { diff --git a/Sources/SecureStorage/SecureStorageDatabaseProvider.swift b/Sources/SecureStorage/SecureStorageDatabaseProvider.swift index 963b14294..f8e3cd723 100644 --- a/Sources/SecureStorage/SecureStorageDatabaseProvider.swift +++ b/Sources/SecureStorage/SecureStorageDatabaseProvider.swift @@ -104,10 +104,18 @@ open class GRDBSecureStorageDatabaseProvider: SecureStorageDatabaseProvider { try FileManager.default.moveItem(at: newDbFile, to: databaseURL) } - public static func databaseFilePath(directoryName: String, fileName: String) -> URL { + public static func databaseFilePath(directoryName: String, fileName: String, appGroupIdentifier: String? = nil) -> URL { let fm = FileManager.default - let subDir = fm.applicationSupportDirectoryForComponent(named: directoryName) + let subDir: URL + if let appGroupIdentifier = appGroupIdentifier { + guard let dir = fm.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else { + fatalError("Failed to get appGroup for identifier \(appGroupIdentifier)") + } + subDir = dir.appendingPathComponent(directoryName) + } else { + subDir = fm.applicationSupportDirectoryForComponent(named: directoryName) + } var isDir: ObjCBool = false if !fm.fileExists(atPath: subDir.path, isDirectory: &isDir) { diff --git a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift index 451178aec..5c69cb8c8 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift @@ -36,7 +36,7 @@ final class BookmarksResponseHandler { let topLevelFoldersSyncables: [SyncableBookmarkAdapter] let bookmarkSyncablesWithoutParent: [SyncableBookmarkAdapter] - let favoritesUUIDs: [String] + let favoritesUUIDs: [String]? var entitiesByUUID: [String: BookmarkEntity] = [:] var idsOfItemsThatRetainModifiedAt = Set() @@ -57,7 +57,7 @@ final class BookmarksResponseHandler { var allUUIDs: Set = [] var childrenToParents: [String: String] = [:] var parentFoldersToChildren: [String: [String]] = [:] - var favoritesUUIDs: [String] = [] + var favoritesUUIDs: [String]? self.received.forEach { syncable in guard let uuid = syncable.uuid else { @@ -113,33 +113,38 @@ final class BookmarksResponseHandler { } try processOrphanedBookmarks() - // populate favorites - if !favoritesUUIDs.isEmpty { - guard let favoritesFolder = BookmarkUtils.fetchFavoritesFolder(context) else { - // Error - unable to process favorites - return - } + processReceivedFavorites() + } - // For non-first sync we rely fully on the server response - if !shouldDeduplicateEntities { - favoritesFolder.favoritesArray.forEach { $0.removeFromFavorites() } - } else if !favoritesFolder.favoritesArray.isEmpty { - // If we're deduplicating and there are favorires locally, we'll need to sync favorites folder back later. - // Let's keep its modifiedAt. - idsOfItemsThatRetainModifiedAt.insert(BookmarkEntity.Constants.favoritesFolderID) - } + // MARK: - Private - favoritesUUIDs.forEach { uuid in - if let bookmark = entitiesByUUID[uuid] { - bookmark.removeFromFavorites() - bookmark.addToFavorites(favoritesRoot: favoritesFolder) - } + private func processReceivedFavorites() { + guard let favoritesUUIDs else { + return + } + + guard let favoritesFolder = BookmarkUtils.fetchFavoritesFolder(context) else { + // Error - unable to process favorites + return + } + + // For non-first sync we rely fully on the server response + if !shouldDeduplicateEntities { + favoritesFolder.favoritesArray.forEach { $0.removeFromFavorites() } + } else if !favoritesFolder.favoritesArray.isEmpty { + // If we're deduplicating and there are favorites locally, we'll need to sync favorites folder back later. + // Let's keep its modifiedAt. + idsOfItemsThatRetainModifiedAt.insert(BookmarkEntity.Constants.favoritesFolderID) + } + + favoritesUUIDs.forEach { uuid in + if let bookmark = entitiesByUUID[uuid] { + bookmark.removeFromFavorites() + bookmark.addToFavorites(favoritesRoot: favoritesFolder) } } } - // MARK: - Private - private func processTopLevelFolder(_ topLevelFolderSyncable: SyncableBookmarkAdapter) throws { guard let topLevelFolderUUID = topLevelFolderSyncable.uuid else { return diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift index c983be43a..e0bd5838e 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift @@ -28,7 +28,7 @@ public protocol EmailManagerSyncSupporting: AnyObject { func getToken() throws -> String? func signIn(username: String, token: String) throws - func signOut() throws + func signOut(isForced: Bool) throws var userDidToggleEmailProtectionPublisher: AnyPublisher { get } } @@ -60,7 +60,7 @@ class EmailProtectionSyncHandler: SettingsSyncHandling { func setValue(_ value: String?) throws { guard let value, let valueData = value.data(using: .utf8) else { - try emailManager.signOut() + try emailManager.signOut(isForced: false) return } diff --git a/Tests/BrowserServicesKitTests/Email/EmailManagerTests.swift b/Tests/BrowserServicesKitTests/Email/EmailManagerTests.swift index d4cccb1e2..44a23bf87 100644 --- a/Tests/BrowserServicesKitTests/Email/EmailManagerTests.swift +++ b/Tests/BrowserServicesKitTests/Email/EmailManagerTests.swift @@ -391,7 +391,9 @@ class MockEmailManagerRequestDelegate: EmailManagerRequestDelegate { var keychainAccessErrorAccessType: EmailKeychainAccessType? var keychainAccessError: EmailKeychainAccessError? - func emailManagerKeychainAccessFailed(accessType: EmailKeychainAccessType, error: EmailKeychainAccessError) { + func emailManagerKeychainAccessFailed(_ emailManager: EmailManager, + accessType: EmailKeychainAccessType, + error: EmailKeychainAccessError) { keychainAccessErrorAccessType = accessType keychainAccessError = error } diff --git a/Tests/BrowserServicesKitTests/Resources/privacy-reference-tests b/Tests/BrowserServicesKitTests/Resources/privacy-reference-tests index 0d23f7680..2e73221f9 160000 --- a/Tests/BrowserServicesKitTests/Resources/privacy-reference-tests +++ b/Tests/BrowserServicesKitTests/Resources/privacy-reference-tests @@ -1 +1 @@ -Subproject commit 0d23f76801c2e73ae7d5ed7daa4af4aca5beec73 +Subproject commit 2e73221f9b5d872e05199db6b29f140406c909ae diff --git a/Tests/BrowserServicesKitTests/SmarterEncryption/BloomFilterWrapperTest.swift b/Tests/BrowserServicesKitTests/SmarterEncryption/BloomFilterWrapperTest.swift index 0c805ae97..79699b66c 100644 --- a/Tests/BrowserServicesKitTests/SmarterEncryption/BloomFilterWrapperTest.swift +++ b/Tests/BrowserServicesKitTests/SmarterEncryption/BloomFilterWrapperTest.swift @@ -32,12 +32,12 @@ class BloomFilterWrapperTest: XCTestCase { } func testWhenBloomFilterEmptyThenContainsIsFalse() { - let testee = BloomFilterWrapper(totalItems: Int32(Constants.filterElementCount), errorRate: Constants.targetErrorRate)! + let testee = BloomFilterWrapper(totalItems: Int32(Constants.filterElementCount), errorRate: Constants.targetErrorRate) XCTAssertFalse(testee.contains("abc")) } func testWhenBloomFilterContainsElementThenContainsIsTrue() { - let testee = BloomFilterWrapper(totalItems: Int32(Constants.filterElementCount), errorRate: Constants.targetErrorRate)! + let testee = BloomFilterWrapper(totalItems: Int32(Constants.filterElementCount), errorRate: Constants.targetErrorRate) testee.add("abc") XCTAssertTrue(testee.contains("abc")) } @@ -46,7 +46,7 @@ class BloomFilterWrapperTest: XCTestCase { let bloomData = createRandomStrings(count: Constants.filterElementCount) let testData = bloomData + createRandomStrings(count: Constants.additionalTestDataElementCount) - let testee = BloomFilterWrapper(totalItems: Int32(bloomData.count), errorRate: Constants.targetErrorRate)! + let testee = BloomFilterWrapper(totalItems: Int32(bloomData.count), errorRate: Constants.targetErrorRate) bloomData.forEach { testee.add($0) } var falsePositives = 0, truePositives = 0, falseNegatives = 0, trueNegatives = 0 diff --git a/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeStoreMock.swift b/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeStoreMock.swift index 081d26770..f14339161 100644 --- a/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeStoreMock.swift +++ b/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeStoreMock.swift @@ -17,6 +17,7 @@ // limitations under the License. // +import Foundation @testable import BrowserServicesKit @testable import BloomFilterWrapper diff --git a/Tests/SecureStorageTests/GRDBSecureStorageDatabaseProviderTests.swift b/Tests/SecureStorageTests/GRDBSecureStorageDatabaseProviderTests.swift index bdb92425c..23b6c7afc 100644 --- a/Tests/SecureStorageTests/GRDBSecureStorageDatabaseProviderTests.swift +++ b/Tests/SecureStorageTests/GRDBSecureStorageDatabaseProviderTests.swift @@ -126,6 +126,12 @@ final class GRDBSecureStorageDatabaseProviderTests: XCTestCase { let databaseFilePath = GRDBSecureStorageDatabaseProvider.databaseFilePath(directoryName: "Test", fileName: "Database.db") XCTAssert(databaseFilePath.absoluteString.hasSuffix("Test/Database.db")) + + let databaseFilePathAppGroup = GRDBSecureStorageDatabaseProvider.databaseFilePath(directoryName: "Test", fileName: "Database.db", appGroupIdentifier: "TEST") + + XCTAssert(databaseFilePathAppGroup.absoluteString.hasSuffix("Test/Database.db")) + + XCTAssertNotEqual(databaseFilePath, databaseFilePathAppGroup) } func createTemporaryFileURL() -> URL { diff --git a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift index 424873249..211cb3c3d 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift @@ -158,6 +158,54 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase }) } + func testWhenPayloadDoesNotContainFavoritesFolderThenFavoritesAreNotAffected() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark(id: "1", isFavorite: true) + Folder(id: "2") { + Bookmark(id: "3", isFavorite: true) + } + } + + let received: [Syncable] = [ + .rootFolder(children: ["1", "2", "4"]), + .bookmark(id: "4") + ] + + let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + Bookmark(id: "1", isFavorite: true) + Folder(id: "2") { + Bookmark(id: "3", isFavorite: true) + } + Bookmark(id: "4") + }) + } + + func testWhenPayloadContainsEmptyFavoritesFolderThenAllFavoritesAreRemoved() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark(id: "1", isFavorite: true) + Folder(id: "2") { + Bookmark(id: "3", isFavorite: true) + } + } + + let received: [Syncable] = [ + .favoritesFolder(favorites: []) + ] + + let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + Bookmark(id: "1") + Folder(id: "2") { + Bookmark(id: "3") + } + }) + } + func testThatSinglePayloadCanCreateReorderAndOrphanBookmarks() async throws { let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType)