diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml index 86c787cb1d..8ccbe6905b 100644 --- a/.github/workflows/publish_dmg_release.yml +++ b/.github/workflows/publish_dmg_release.yml @@ -109,8 +109,8 @@ jobs: - name: Upload to S3 id: upload env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} run: | # Back up existing appcast2.xml diff --git a/Configuration/App/DuckDuckGoAppStore.xcconfig b/Configuration/App/DuckDuckGoAppStore.xcconfig index 3ee212ad5e..904caca8c5 100644 --- a/Configuration/App/DuckDuckGoAppStore.xcconfig +++ b/Configuration/App/DuckDuckGoAppStore.xcconfig @@ -17,11 +17,6 @@ #include "../AppStore.xcconfig" #include "ManualAppStoreRelease.xcconfig" -AGENT_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review - PRODUCT_BUNDLE_IDENTIFIER = $(MAIN_BUNDLE_IDENTIFIER) CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAppStore.entitlements diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index c2ae87c9b5..0ad3f9f6b5 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -50,21 +50,23 @@ AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN App Store AGENT_RELEASE_PRODUCT_NAME = DuckDuckGo VPN -SYSEX_BUNDLE_ID[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -SYSEX_BUNDLE_ID[config=Debug][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=CI][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.debug.network-protection-extension -SYSEX_BUNDLE_ID[config=Review][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension -SYSEX_BUNDLE_ID[config=Release][sdk=*] = com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension +// Extensions -// Distributed Notifications Prefix +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).proxy +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).proxy + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(AGENT_BUNDLE_ID).network-protection-extension -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension +// Distributed Notifications Prefix -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) +DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(AGENT_BUNDLE_ID_BASE).network-extension DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 060339a249..550b71a3ee 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 122 +CURRENT_PROJECT_VERSION = 123 diff --git a/Configuration/DeveloperID.xcconfig b/Configuration/DeveloperID.xcconfig index 0bfb9bb8cb..b66acc76d2 100644 --- a/Configuration/DeveloperID.xcconfig +++ b/Configuration/DeveloperID.xcconfig @@ -65,6 +65,20 @@ AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review AGENT_PRODUCT_NAME = DuckDuckGo VPN +// Extensions + +PROXY_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +PROXY_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + +TUNNEL_EXTENSION_BUNDLE_ID[sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Debug][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Review][sdk=*] = $(SYSEX_BUNDLE_ID) +TUNNEL_EXTENSION_BUNDLE_ID[config=Release][sdk=*] = $(SYSEX_BUNDLE_ID) + // DBP DBP_BACKGROUND_AGENT_PRODUCT_NAME = DuckDuckGo Personal Information Removal diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig index 60ad407569..2fb095fc56 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig @@ -14,10 +14,7 @@ // #include "../ExtensionBase.xcconfig" - -// Since we're using nonstandard bundle IDs we'll just define them here, but we should consider -// standardizing the bundle IDs so we can just define BUNDLE_IDENTIFIER_PREFIX -BUNDLE_IDENTIFIER_PREFIX = com.duckduckgo.mobile.ios.vpn.agent +#include "../../AppStore.xcconfig" CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -38,17 +35,11 @@ FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -NETP_BASE_APP_GROUP = $(DEVELOPMENT_TEAM).com.duckduckgo.macos.browser.network-protection -NETP_APP_GROUP[config=CI][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Review][sdk=macos*] = $(NETP_BASE_APP_GROUP).review -NETP_APP_GROUP[config=Debug][sdk=macos*] = $(NETP_BASE_APP_GROUP).debug -NETP_APP_GROUP[config=Release][sdk=macos*] = $(NETP_BASE_APP_GROUP) - PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = -PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug.network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).network-protection-extension -PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).review.network-protection-extension +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos @@ -59,24 +50,3 @@ SKIP_INSTALL = YES SWIFT_EMIT_LOC_STRINGS = YES LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks - -// Distributed Notifications: - -AGENT_BUNDLE_ID_BASE[sdk=*] = com.duckduckgo.mobile.ios.vpn.agent -AGENT_BUNDLE_ID[sdk=*] = $(AGENT_BUNDLE_ID_BASE) -AGENT_BUNDLE_ID[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).debug -AGENT_BUNDLE_ID[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).review - -SYSEX_BUNDLE_ID_BASE[sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Debug][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=CI][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Review][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension -SYSEX_BUNDLE_ID_BASE[config=Release][sdk=*] = $(AGENT_BUNDLE_ID_BASE).network-extension - -DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE = $(SYSEX_BUNDLE_ID_BASE) - -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=CI][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).ci -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Review][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).review -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Debug][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE).debug -DISTRIBUTED_NOTIFICATIONS_PREFIX[config=Release][sdk=*] = $(DISTRIBUTED_NOTIFICATIONS_PREFIX_BASE) diff --git a/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig new file mode 100644 index 0000000000..5f70d87091 --- /dev/null +++ b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig @@ -0,0 +1,52 @@ +// 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. +// + +#include "../ExtensionBase.xcconfig" +#include "../../AppStore.xcconfig" + +CODE_SIGN_ENTITLEMENTS[config=CI][sdk=macosx*] = +CODE_SIGN_ENTITLEMENTS[config=Debug][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Release][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_ENTITLEMENTS[config=Review][sdk=macosx*] = VPNProxyExtension/VPNProxyExtension.entitlements +CODE_SIGN_STYLE[config=Debug][sdk=*] = Automatic + +CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application +CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development +CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = + +GENERATE_INFOPLIST_FILE = YES +INFOPLIST_FILE = VPNProxyExtension/Info.plist +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. + +FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION + +PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Release][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) + +PROVISIONING_PROFILE_SPECIFIER[config=CI][sdk=macosx*] = +PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos +PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = match AppStore $(AGENT_BUNDLE_ID).proxy macos + +SDKROOT = macosx +SKIP_INSTALL = YES +SWIFT_EMIT_LOC_STRINGS = YES + +LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks @executable_path/../../../../Frameworks diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0f1e8a2d3e..68632bf6e3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1099,12 +1099,11 @@ 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B2537722A11BF8B00610219 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B25376F2A11BF8B00610219 /* main.swift */; }; 4B2537772A11BFE100610219 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2537762A11BFE100610219 /* PixelKit */; }; - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B29759728281F0900187C4E /* FirefoxEncryptionKeyReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29759628281F0900187C4E /* FirefoxEncryptionKeyReader.swift */; }; 4B2975992828285900187C4E /* FirefoxKeyReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2975982828285900187C4E /* FirefoxKeyReaderTests.swift */; }; 4B2AAAF529E70DEA0026AFC0 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2AAAF429E70DEA0026AFC0 /* Lottie */; }; - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2D062B2A11C0E100DE1F49 /* Networking */; }; 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; @@ -1168,17 +1167,16 @@ 4B44FEF52B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */; }; 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */; }; 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */; }; 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4BEC482A11B61F001D9AC5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B4BEC342A11B509001D9AC5 /* Assets.xcassets */; }; 4B4D603F2A0B290200BCD287 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4D60982A0B2A5C00BCD287 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4B4D60972A0B2A5C00BCD287 /* PixelKit */; }; 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */; }; 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; @@ -1202,7 +1200,7 @@ 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4D60DF2A0C875F00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */; }; @@ -1528,7 +1526,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0AACAD28BC6FD0001038AC /* SafariFaviconsReader.swift */; }; 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */; }; 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65E6B9F26D9F10600095F96 /* NSBezierPathExtension.swift */; }; - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6820E325502F19005ED0D5 /* WebsiteDataStore.swift */; }; 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */; }; 4B957A482AC7AE700062CA31 /* PermissionContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C852926942AC90048FEBE /* PermissionContextMenu.swift */; }; @@ -2049,7 +2047,7 @@ 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8F52402A18326600BE7131 /* NetworkProtectionTunnelController.swift */; }; 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 4BA7C4DC2B3F64E500AFE511 /* LoginItems */; }; - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BB6CE5F26B77ED000EC5860 /* Cryptography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */; }; 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */; }; 4BB88B4A25B7B690006F6B06 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -2133,7 +2131,7 @@ 4BF97AD62B43C45800EB4240 /* NetworkProtectionNavBarPopoverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3618C12ADE75C8000D6154 /* NetworkProtectionNavBarPopoverManager.swift */; }; 4BF97AD72B43C53D00EB4240 /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */; }; - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */; }; 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */; }; 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */; }; @@ -2174,6 +2172,14 @@ 56D145F229E6F06D00E3488A /* MockBookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */; }; 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */; }; + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */; }; + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */; }; 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; 7B1E819F27C8874900FF0E60 /* ContentOverlay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */; }; 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */; }; @@ -2190,15 +2196,25 @@ 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4CE8E626F02134009134B1 /* TabBarTests.swift */; }; 7B5DD69A2AE51FFA001DE99C /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5DD6992AE51FFA001DE99C /* PixelKit */; }; 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B5F9A742AE2BE4E002AEBC0 /* PixelKit */; }; + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7B7DFB212B7E7473009EA1A3 /* Networking */; }; 7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B8C083B2AE1268E00F4C67F /* PixelKit */; }; 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */; }; 7B934C412A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */; }; + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */; }; + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD5A2B7E0B85004FEF43 /* Common */; }; + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7B97CD612B7E0C4B004FEF43 /* PixelKit */; }; + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */; }; 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C42667104B00AD2C21 /* CoreDataTestUtilities.swift */; }; 7BA59C9B2AE18B49009A97B1 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */; }; 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */; }; - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */; }; 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */; }; 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */; }; @@ -2214,8 +2230,8 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606A2A0B29FA00BCD287 /* NetworkProtectionControllerErrorStore.swift */; }; 7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 7BA7CC502AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */; }; + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; @@ -2233,9 +2249,13 @@ 7BBD44282AD730A400D0A064 /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBD44272AD730A400D0A064 /* PixelKit */; }; 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */; }; 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */; }; 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B4D603E2A0B290200BCD287 /* NetworkExtension.framework */; }; + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */ = {isa = PBXBuildFile; fileRef = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BE146082A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */; }; 7BEC182F2AD5D8DC00D30536 /* SystemExtensionManager in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */; }; @@ -3104,7 +3124,6 @@ EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; EECE10E529DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; EECE10E629DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; @@ -3185,6 +3204,13 @@ remoteGlobalIDString = AA585D7D248FD31100E9A3E2; remoteInfo = "DuckDuckGo Privacy Browser"; }; + 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7BDA36E42B7E037100AD5388; + remoteInfo = VPNProxyExtension; + }; 7BEC18302AD5DA3300D30536 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AA585D76248FD31100E9A3E2 /* Project object */; @@ -3242,14 +3268,16 @@ name = "Embed Login Items"; runOnlyForDeploymentPostprocessing = 0; }; - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */ = { + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in CopyFiles */, + 7BDA36F92B7E084A00AD5388 /* VPNProxyExtension.appex in Embed Network Extensions */, + 4BA7C4E12B3F6F8600AFE511 /* NetworkProtectionAppExtension.appex in Embed Network Extensions */, ); + name = "Embed Network Extensions"; runOnlyForDeploymentPostprocessing = 0; }; B6EC37E629B5DA2A001ACE79 /* CopyFiles */ = { @@ -3546,7 +3574,7 @@ 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionSystemExtension.xcconfig; sourceTree = ""; }; 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetworkProtectionAppExtension.xcconfig; sourceTree = ""; }; 4B4D60512A0B293C00BCD287 /* ExtensionBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ExtensionBase.xcconfig; sourceTree = ""; }; - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionBundle.swift; sourceTree = ""; }; + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+VPN.swift"; sourceTree = ""; }; 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOptionKeyExtension.swift; sourceTree = ""; }; 4B4D60652A0B29FA00BCD287 /* NetworkProtectionNavBarButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNavBarButtonModel.swift; sourceTree = ""; }; 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+ConvenienceInitializers.swift"; sourceTree = ""; }; @@ -3556,10 +3584,7 @@ 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteCodeViewModel.swift; sourceTree = ""; }; 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = ""; }; 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUNNotificationsPresenter.swift; sourceTree = ""; }; - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionExtensionMachService.swift; sourceTree = ""; }; 4B4D607C2A0B29FA00BCD287 /* UserText+NetworkProtectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtectionExtensions.swift"; sourceTree = ""; }; - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Release.entitlements; sourceTree = ""; }; - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGo_NetP_Debug.entitlements; sourceTree = ""; }; 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserText+NetworkProtection.swift"; sourceTree = ""; }; 4B4D60E12A0C883A00BCD287 /* AppMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMain.swift; sourceTree = ""; }; 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; @@ -3766,7 +3791,10 @@ 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataImportProviderTests.swift; sourceTree = ""; }; 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowManager.swift; sourceTree = ""; }; + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = ""; }; 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = ""; }; 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = ""; }; @@ -3790,7 +3818,6 @@ 7BA7CC0B2AD11D1E0042E5CE /* DuckDuckGoVPNAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPNAppStore.xcconfig; sourceTree = ""; }; 7BA7CC0C2AD11D1E0042E5CE /* DuckDuckGoVPN.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoVPN.xcconfig; sourceTree = ""; }; 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckDuckGoVPNAppDelegate.swift; sourceTree = ""; }; - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Configuration.swift"; sourceTree = ""; }; 7BA7CC102AD11DC80042E5CE /* Info-AppStore.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-AppStore.plist"; sourceTree = ""; }; 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelControllerIPCService.swift; sourceTree = ""; }; 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -3809,6 +3836,10 @@ 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; 7BD8679A2A9E9E000063B9F7 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNProxyExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BDA36EA2B7E037200AD5388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VPNProxyExtension.entitlements; sourceTree = ""; }; + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = VPNProxyExtension.xcconfig; sourceTree = ""; }; 7BE146062A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugMenu.swift; sourceTree = ""; }; 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SystemExtensionManager; sourceTree = ""; }; 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkPopoverView.swift; sourceTree = ""; }; @@ -4443,6 +4474,7 @@ 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */, 7B5F9A752AE2BE4E002AEBC0 /* PixelKit in Frameworks */, 4BF97AD32B43C43F00EB4240 /* NetworkProtectionUI in Frameworks */, + 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */, B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, 984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */, 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, @@ -4510,6 +4542,7 @@ 37269F012B332FC8005E8E46 /* Common in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, + 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, ); @@ -4520,6 +4553,7 @@ buildActionMask = 2147483647; files = ( 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, + 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, @@ -4536,6 +4570,7 @@ 7BFCB7502ADE7E2300DA3EA7 /* PixelKit in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, + 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */, 4B2D067F2A1334D700DE1F49 /* NetworkProtectionUI in Frameworks */, 4BA7C4DD2B3F64E500AFE511 /* LoginItems in Frameworks */, @@ -4571,6 +4606,7 @@ 3143C8792B0D1F3D00382627 /* DataBrokerProtection in Frameworks */, 372217842B33380E00B8E9C2 /* TestUtils in Frameworks */, 4B957BD62AC7AE700062CA31 /* LoginItems in Frameworks */, + 7B94E1652B7ED95100E32B96 /* NetworkProtectionProxy in Frameworks */, 4B957BD72AC7AE700062CA31 /* NetworkProtection in Frameworks */, 4B957BD82AC7AE700062CA31 /* BrowserServicesKit in Frameworks */, 4B957BDA2AC7AE700062CA31 /* Bookmarks in Frameworks */, @@ -4613,6 +4649,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E22B7E037100AD5388 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDA36E62B7E037100AD5388 /* NetworkExtension.framework in Frameworks */, + 7B97CD592B7E0B57004FEF43 /* NetworkProtectionProxy in Frameworks */, + 7B97CD622B7E0C4B004FEF43 /* PixelKit in Frameworks */, + 7B7DFB222B7E7473009EA1A3 /* Networking in Frameworks */, + 7B97CD5B2B7E0B85004FEF43 /* Common in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C62AAA39A70026E7DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -4661,6 +4709,7 @@ 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, + 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, 98A50964294B691800D10880 /* Persistence in Frameworks */, ); @@ -5240,8 +5289,9 @@ 4B18E32C2A1ECF1F005D0AAA /* NetworkProtection */ = { isa = PBXGroup; children = ( - 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, 4B4D60502A0B293C00BCD287 /* NetworkProtectionAppExtension.xcconfig */, + 4B4D604F2A0B293C00BCD287 /* NetworkProtectionSystemExtension.xcconfig */, + 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */, ); path = NetworkProtection; sourceTree = ""; @@ -5379,7 +5429,7 @@ 4B4D605D2A0B29FA00BCD287 /* AppAndExtensionAndNotificationTargets */ = { isa = PBXGroup; children = ( - 4B4D605E2A0B29FA00BCD287 /* NetworkProtectionBundle.swift */, + 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */, 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */, B602E8152A1E2570006D261F /* URL+NetworkProtection.swift */, ); @@ -5463,7 +5513,6 @@ isa = PBXGroup; children = ( B602E81F2A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift */, - 4B4D60782A0B29FA00BCD287 /* NetworkProtectionExtensionMachService.swift */, ); path = SystemExtensionAndNotificationTargets; sourceTree = ""; @@ -5481,6 +5530,7 @@ children = ( 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, + 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6126,11 +6176,11 @@ isa = PBXGroup; children = ( 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, - 7BA7CC0F2AD11DC80042E5CE /* Bundle+Configuration.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, 7BA7CC152AD11DC80042E5CE /* NetworkProtectionBouncer.swift */, 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */, + 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */, 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */, 7BA7CC172AD11DC80042E5CE /* UserText.swift */, 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */, @@ -6160,6 +6210,15 @@ path = LetsMove1.25; sourceTree = ""; }; + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */ = { + isa = PBXGroup; + children = ( + 7BDA36EA2B7E037200AD5388 /* Info.plist */, + 7BDA36EB2B7E037200AD5388 /* VPNProxyExtension.entitlements */, + ); + path = VPNProxyExtension; + sourceTree = ""; + }; 853014D425E6709500FB8205 /* Support */ = { isa = PBXGroup; children = ( @@ -6523,6 +6582,7 @@ 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */, 7BD01C182AD8319C0088B32E /* IPCServiceManager.swift */, + 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, @@ -6646,6 +6706,7 @@ B6EC37E929B5DA2A001ACE79 /* tests-server */, 7B96D0D02ADFDA7F007E02C8 /* DuckDuckGoDBPTests */, 4B5F14F72A148B230060320F /* NetworkProtectionAppExtension */, + 7BDA36E72B7E037200AD5388 /* VPNProxyExtension */, 4B25375C2A11BE7500610219 /* NetworkProtectionSystemExtension */, 9D9AE9132AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent */, 7BA7CC0D2AD11DC80042E5CE /* DuckDuckGoVPN */, @@ -6676,6 +6737,7 @@ 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */, 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */, 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */, + 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */, ); name = Products; sourceTree = ""; @@ -6746,8 +6808,6 @@ 4B5F15032A1570F10060320F /* DuckDuckGoDebug.entitlements */, 37D9BBA329376EE8000B99F9 /* DuckDuckGoAppStore.entitlements */, 377E54382937B7C400780A0A /* DuckDuckGoAppStoreCI.entitlements */, - 4B4D609E2A0B2C2300BCD287 /* DuckDuckGo_NetP_Debug.entitlements */, - 4B4D609C2A0B2C2300BCD287 /* DuckDuckGo_NetP_Release.entitlements */, 4B2D06642A132F3A00DE1F49 /* NetworkProtectionAppExtension.entitlements */, 4B5F14C42A145D6A0060320F /* NetworkProtectionVPNController.entitlements */, 56CEE9092B7A66C500CF10AA /* Info.plist */, @@ -8280,6 +8340,7 @@ 4BF97AD42B43C43F00EB4240 /* NetworkProtection */, 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */, 312978892B64131200B67619 /* DataBrokerProtection */, + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -8395,6 +8456,7 @@ 4B2D062B2A11C0E100DE1F49 /* Networking */, EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -8425,6 +8487,7 @@ 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */, 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -8438,11 +8501,12 @@ 4B2D06662A13318400DE1F49 /* Frameworks */, 4B2D06672A13318400DE1F49 /* Resources */, 4B2D067D2A13341200DE1F49 /* ShellScript */, - 4BA7C4E02B3F6F7500AFE511 /* CopyFiles */, + 4BA7C4E02B3F6F7500AFE511 /* Embed Network Extensions */, ); buildRules = ( ); dependencies = ( + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */, 4BA7C4DF2B3F6F4900AFE511 /* PBXTargetDependency */, B6080BA52B20AF8800B418EF /* PBXTargetDependency */, ); @@ -8453,6 +8517,7 @@ 7BA7CC602AD1211C0042E5CE /* Networking */, 7BEEA5132AD1236300A9E72B /* NetworkProtectionIPC */, 7BFCB74F2ADE7E2300DA3EA7 /* PixelKit */, + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */, 4BA7C4DC2B3F64E500AFE511 /* LoginItems */, ); productName = DuckDuckGoAgentAppStore; @@ -8552,6 +8617,7 @@ 372217832B33380E00B8E9C2 /* TestUtils */, 373FB4B42B4D6C57004C88D6 /* PreferencesViews */, 1E21F8E22B73E48600FB272E /* Subscription */, + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */; @@ -8596,6 +8662,29 @@ productReference = 7B4CE8DA26F02108009134B1 /* UI Tests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */; + buildPhases = ( + 7BDA36E12B7E037100AD5388 /* Sources */, + 7BDA36E22B7E037100AD5388 /* Frameworks */, + 7BDA36E32B7E037100AD5388 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = VPNProxyExtension; + packageProductDependencies = ( + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */, + 7B97CD5A2B7E0B85004FEF43 /* Common */, + 7B97CD612B7E0C4B004FEF43 /* PixelKit */, + 7B7DFB212B7E7473009EA1A3 /* Networking */, + ); + productName = VPNProxyExtension; + productReference = 7BDA36E52B7E037100AD5388 /* VPNProxyExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 9D9AE8B22AAA39A70026E7DC /* DuckDuckGoDBPBackgroundAgent */ = { isa = PBXNativeTarget; buildConfigurationList = 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */; @@ -8690,6 +8779,7 @@ 37269EFA2B332F9E005E8E46 /* Common */, 3722177F2B3337FE00B8E9C2 /* TestUtils */, 373FB4B02B4D6C42004C88D6 /* PreferencesViews */, + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -8746,7 +8836,7 @@ AA585D76248FD31100E9A3E2 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1400; ORGANIZATIONNAME = DuckDuckGo; TargetAttributes = { @@ -8788,6 +8878,9 @@ CreatedOnToolsVersion = 12.5.1; TestTargetID = AA585D7D248FD31100E9A3E2; }; + 7BDA36E42B7E037100AD5388 = { + CreatedOnToolsVersion = 15.2; + }; AA585D7D248FD31100E9A3E2 = { CreatedOnToolsVersion = 11.5; }; @@ -8833,6 +8926,7 @@ 3706FE9B293F662100E42796 /* Integration Tests App Store */, B6EC37E729B5DA2A001ACE79 /* tests-server */, 4B4D603C2A0B290200BCD287 /* NetworkProtectionAppExtension */, + 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */, 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */, 4B4BEC1F2A11B4E2001D9AC5 /* DuckDuckGoNotifications */, 4B2D06382A11CFBA00DE1F49 /* DuckDuckGoVPN */, @@ -9050,6 +9144,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E32B7E037100AD5388 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8C92AAA39A70026E7DC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -9861,7 +9962,7 @@ B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, - 4BF97AD92B43C5C000EB4240 /* NetworkProtectionBundle.swift in Sources */, + 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, B626A7552991413000053070 /* SerpHeadersNavigationResponder.swift in Sources */, @@ -10520,11 +10621,11 @@ 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, - 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */, + 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */, B65DA5F32A77D3C700CBEE8D /* UserDefaultsWrapper.swift in Sources */, 4B2537722A11BF8B00610219 /* main.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, - 4B2D06292A11C0C900DE1F49 /* NetworkProtectionBundle.swift in Sources */, + 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10543,14 +10644,14 @@ 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, - 7BA7CC532AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, - 7BA7CC3C2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, + 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, 7BFE95562A9DF29B0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, + 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, @@ -10569,10 +10670,10 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, + 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, - 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -10590,7 +10691,7 @@ 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, 7BA7CC432AD11E480042E5CE /* UserText.swift in Sources */, - 7BA7CC542AD11FCE0042E5CE /* NetworkProtectionBundle.swift in Sources */, + 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10601,12 +10702,11 @@ 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */, 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */, 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4BEC432A11B5C7001D9AC5 /* NetworkProtectionBundle.swift in Sources */, + 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, - 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */, EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10617,14 +10717,14 @@ files = ( 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, + 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, - 4B4D60A02A0B2D5B00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, 4B4D60AD2A0C807300BCD287 /* NSApplicationExtension.swift in Sources */, 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */, - EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, @@ -10909,7 +11009,7 @@ 4B957A422AC7AE700062CA31 /* SafariFaviconsReader.swift in Sources */, 4B957A432AC7AE700062CA31 /* NSScreenExtension.swift in Sources */, 4B957A442AC7AE700062CA31 /* NSBezierPathExtension.swift in Sources */, - 4B957A452AC7AE700062CA31 /* NetworkProtectionBundle.swift in Sources */, + 4B957A452AC7AE700062CA31 /* Bundle+VPN.swift in Sources */, B68D21CA2ACBC971002DA3C2 /* MockPrivacyConfiguration.swift in Sources */, 4B957A462AC7AE700062CA31 /* WebsiteDataStore.swift in Sources */, 4B957A472AC7AE700062CA31 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -11378,11 +11478,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7BDA36E12B7E037100AD5388 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B97CD5C2B7E0BBB004FEF43 /* UserDefaultsWrapper.swift in Sources */, + 7B97CD602B7E0C2E004FEF43 /* Logging.swift in Sources */, + 7B97CD5E2B7E0BEA004FEF43 /* OptionalExtension.swift in Sources */, + 7B97CD5F2B7E0BF7004FEF43 /* NSApplicationExtension.swift in Sources */, + 7BDA36F52B7E055800AD5388 /* MacTransparentProxyProvider.swift in Sources */, + 7B97CD5D2B7E0BCE004FEF43 /* BundleExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9D9AE8B62AAA39A70026E7DC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, @@ -11394,6 +11508,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, @@ -11677,7 +11792,7 @@ 4B0AACAE28BC6FD0001038AC /* SafariFaviconsReader.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, - 4B4D60E02A0C875F00BCD287 /* NetworkProtectionBundle.swift in Sources */, + 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, @@ -12449,6 +12564,11 @@ target = AA585D7D248FD31100E9A3E2 /* DuckDuckGo Privacy Browser */; targetProxy = 7B4CE8DF26F02108009134B1 /* PBXContainerItemProxy */; }; + 7BDA36F82B7E082100AD5388 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7BDA36E42B7E037100AD5388 /* VPNProxyExtension */; + targetProxy = 7BDA36F72B7E082100AD5388 /* PBXContainerItemProxy */; + }; 7BEC18312AD5DA3300D30536 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4B2537592A11BE7300610219 /* NetworkProtectionSystemExtension */; @@ -12918,6 +13038,34 @@ }; name = Release; }; + 7BDA36F02B7E037200AD5388 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 7BDA36F12B7E037200AD5388 /* CI */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = CI; + }; + 7BDA36F22B7E037200AD5388 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 7BDA36F32B7E037200AD5388 /* Review */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA36F62B7E06A300AD5388 /* VPNProxyExtension.xcconfig */; + buildSettings = { + }; + name = Review; + }; 9D9AE8CD2AAA39A70026E7DC /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7B6EC5E52AE2D8AF004FE6DF /* DuckDuckGoDBPAgent.xcconfig */; @@ -13225,6 +13373,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7BDA36F42B7E037200AD5388 /* Build configuration list for PBXNativeTarget "VPNProxyExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7BDA36F02B7E037200AD5388 /* Debug */, + 7BDA36F12B7E037200AD5388 /* CI */, + 7BDA36F22B7E037200AD5388 /* Release */, + 7BDA36F32B7E037200AD5388 /* Review */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9D9AE8CC2AAA39A70026E7DC /* Build configuration list for PBXNativeTarget "DuckDuckGoDBPBackgroundAgent" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -13379,7 +13538,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 109.0.0; + version = 109.0.2; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -13768,6 +13927,18 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = NetworkProtection; }; + 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B1459562B7D43E500047F2C /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7B31FD8B2AD125620086AA24 /* NetworkProtectionIPC */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionIPC; @@ -13784,10 +13955,36 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B7DFB212B7E7473009EA1A3 /* Networking */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Networking; + }; 7B8C083B2AE1268E00F4C67F /* PixelKit */ = { isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD582B7E0B57004FEF43 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; + 7B97CD5A2B7E0B85004FEF43 /* Common */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Common; + }; + 7B97CD612B7E0C4B004FEF43 /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + productName = PixelKit; + }; + 7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BA59C9A2AE18B49009A97B1 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; @@ -13806,6 +14003,10 @@ isa = XCSwiftPackageProductDependency; productName = PixelKit; }; + 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */ = { + isa = XCSwiftPackageProductDependency; + productName = NetworkProtectionProxy; + }; 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */ = { isa = XCSwiftPackageProductDependency; productName = SystemExtensionManager; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4796599b2d..dbe440a602 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "5ecf4fe56f334be6eaecb65f6d55632a6d53921c", - "version" : "109.0.0" + "revision" : "da5f8ae73e7ad7fc47931f82f5ac6c4fafa6ac94", + "version" : "109.0.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "063b560e59a50e03d9b00b88a7fcb2ed2b562395", - "version" : "4.61.0" + "revision" : "36ddba2cbac52a41b9a9275af06d32fa8a56d2d7", + "version" : "4.64.0" } }, { diff --git a/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json new file mode 100644 index 0000000000..3fe9b59242 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json new file mode 100644 index 0000000000..802fa68a4c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf new file mode 100644 index 0000000000..de1f4718ed Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json new file mode 100644 index 0000000000..b609317961 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-Mute-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf new file mode 100644 index 0000000000..635e45f874 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json new file mode 100644 index 0000000000..35d4dda319 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index a2eb4706e4..4c70d4bacc 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -29,6 +29,12 @@ extension WKWebView { return false } + enum AudioState { + case muted + case unmuted + case notSupported + } + enum CaptureState { case none case active @@ -129,6 +135,48 @@ extension WKWebView { } #endif + func muteOrUnmute() { +#if !APPSTORE + guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { + assertionFailure("WKWebView does not respond to selector _stopMediaCapture") + return + } + let mutedState: _WKMediaMutedState = { + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { return [] } + return self._mediaMutedState() + }() + var newState = mutedState + + if newState == .audioMuted { + newState.remove(.audioMuted) + } else { + newState.insert(.audioMuted) + } + guard newState != mutedState else { return } + self._setPageMuted(newState) +#endif + } + + /// Returns the audio state of the WKWebView. + /// + /// - Returns: `muted` if the web view is muted + /// `unmuted` if the web view is unmuted + /// `notSupported` if the web view does not support fetching the current audio state + func audioState() -> AudioState { +#if APPSTORE + return .notSupported +#else + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { + assertionFailure("WKWebView does not respond to selector _mediaMutedState") + return .notSupported + } + + let mutedState = self._mediaMutedState() + + return mutedState.contains(.audioMuted) ? .muted : .unmuted +#endif + } + func stopMediaCapture() { guard #available(macOS 12.0, *) else { #if !APPSTORE diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index c3f8ff0f68..eee5b3f4e5 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -199,6 +199,8 @@ struct UserText { static let pinTab = NSLocalizedString("pin.tab", value: "Pin Tab", comment: "Menu item. Pin as a verb") static let unpinTab = NSLocalizedString("unpin.tab", value: "Unpin Tab", comment: "Menu item. Unpin as a verb") static let closeTab = NSLocalizedString("close.tab", value: "Close Tab", comment: "Menu item") + static let muteTab = NSLocalizedString("mute.tab", value: "Mute Tab", comment: "Menu item. Mute tab") + static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute Tab", comment: "Menu item. Unmute tab") static let closeOtherTabs = NSLocalizedString("close.other.tabs", value: "Close Other Tabs", comment: "Menu item") static let closeTabsToTheRight = NSLocalizedString("close.tabs.to.the.right", value: "Close Tabs to the Right", comment: "Menu item") static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index ab0e4f783b..4ffcdc68c8 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -49,8 +49,8 @@ final class DBPHomeViewController: NSViewController { let privacySettings = PrivacySecurityPreferences.shared let sessionKey = UUID().uuidString let prefs = ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, - sessionKey: sessionKey, - featureToggles: features) + sessionKey: sessionKey, + featureToggles: features) return DataBrokerProtectionViewController( scheduler: dataBrokerProtectionManager.scheduler, @@ -191,7 +191,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Bool { redeemUseCase.shouldAskForInviteCode() } + + // MARK: - Debugging Features + + public func showAgentIPAddress() { + ipcClient.openBrowser(domain: "https://www.whatismyip.com") + } } extension DataBrokerProtectionManager: DataBrokerProtectionDataManagerDelegate { diff --git a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift index e1e00e38c7..cdbfa623a9 100644 --- a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift +++ b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Foundation import LoginItems #if DBP diff --git a/DuckDuckGo/DuckDuckGo.entitlements b/DuckDuckGo/DuckDuckGo.entitlements index 7b79b8b2fe..757dc88e2c 100644 --- a/DuckDuckGo/DuckDuckGo.entitlements +++ b/DuckDuckGo/DuckDuckGo.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index e419bc0920..97443cb452 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -19,6 +19,11 @@ com.apple.security.files.user-selected.read-write + com.apple.developer.networking.networkextension + + packet-tunnel-provider + app-proxy-provider + com.apple.security.network.client com.apple.security.personal-information.location diff --git a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements index a2c7bd6bd5..13ea43d233 100644 --- a/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStoreCI.entitlements @@ -2,10 +2,6 @@ - com.apple.developer.networking.networkextension - - packet-tunnel-provider - com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/DuckDuckGo/DuckDuckGoDebug.entitlements b/DuckDuckGo/DuckDuckGoDebug.entitlements index dcffb16791..dad1686cba 100644 --- a/DuckDuckGo/DuckDuckGoDebug.entitlements +++ b/DuckDuckGo/DuckDuckGoDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements deleted file mode 100644 index 069c866e05..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Debug.entitlements +++ /dev/null @@ -1,30 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - HKE973VLUW.com.duckduckgo.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - - diff --git a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements b/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements deleted file mode 100644 index a2226d1f8d..0000000000 --- a/DuckDuckGo/DuckDuckGo_NetP_Release.entitlements +++ /dev/null @@ -1,38 +0,0 @@ - - - - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - com.apple.developer.usernotifications.time-sensitive - - com.apple.security.application-groups - - $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection - $(NETP_APP_GROUP) - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.location - - keychain-access-groups - - $(AppIdentifierPrefix)com.duckduckgo.macos.browser - $(AppIdentifierPrefix)com.duckduckgo.network-protection - - com.apple.security.personal-information.location - - com.apple.developer.networking.networkextension - - packet-tunnel-provider-systemextension - - com.apple.developer.system-extension.install - - - diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index 4d7c2d94c0..70d7389fb7 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -8,7 +8,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "DuckDuckGo" + "value" : "DuckDuckGo Privacy Pro" } } } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index faef1b6820..ddf6e62331 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -5092,6 +5092,18 @@ } } }, + "mute.tab" : { + "comment" : "Menu item. Mute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Mute Tab" + } + } + } + }, "n.more.tabs" : { "comment" : "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window", "extractionState" : "extracted_with_value", @@ -9100,6 +9112,18 @@ } } }, + "unmute.tab" : { + "comment" : "Menu item. Unmute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unmute Tab" + } + } + } + }, "unpin.tab" : { "comment" : "Menu item. Unpin as a verb", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift new file mode 100644 index 0000000000..169f0ceb50 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/Bundle+VPN.swift @@ -0,0 +1,65 @@ +// +// Bundle+VPN.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 +import NetworkProtection + +extension Bundle { + + private enum VPNInfoKey: String { + case tunnelExtensionBundleID = "TUNNEL_EXTENSION_BUNDLE_ID" + case proxyExtensionBundleID = "PROXY_EXTENSION_BUNDLE_ID" + } + + static var tunnelExtensionBundleID: String { + string(for: .tunnelExtensionBundleID) + } + + static var proxyExtensionBundleID: String { + string(for: .proxyExtensionBundleID) + } + + private static func string(for key: VPNInfoKey) -> String { + guard let bundleID = Bundle.main.object(forInfoDictionaryKey: key.rawValue) as? String else { + fatalError("Info.plist is missing \(key)") + } + + return bundleID + } + +#if !NETWORK_EXTENSION + // for the Main or Launcher Agent app + static func mainAppBundle() -> Bundle { + return Bundle.main + } +#elseif NETP_SYSTEM_EXTENSION + // for the System Extension (Developer ID) + static func mainAppBundle() -> Bundle { + return Bundle(url: .mainAppBundleURL)! + } + // AppEx (App Store) can‘t access Main App Bundle +#endif + + static let keychainType: KeychainType = { +#if NETP_SYSTEM_EXTENSION + .system +#else + .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) +#endif + }() +} diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift deleted file mode 100644 index e14b7f1e84..0000000000 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionBundle.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// NetworkProtectionBundle.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 -import NetworkProtection - -enum NetworkProtectionBundle { - -#if !NETWORK_EXTENSION - // for the Main or Launcher Agent app - static func mainAppBundle() -> Bundle { - return Bundle.main - } -#elseif NETP_SYSTEM_EXTENSION - // for the System Extension (Developer ID) - static func mainAppBundle() -> Bundle { - return Bundle(url: .mainAppBundleURL)! - } - // AppEx (App Store) can‘t access Main App Bundle -#endif - - static func extensionBundle() -> Bundle { -#if NETWORK_EXTENSION // When this code is compiled for any network-extension - return Bundle.main -#elseif NETP_SYSTEM_EXTENSION // When this code is compiled for the app when configured to use the sysex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#else // When this code is compiled for the app when configured to use the appex - let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Plugins", relativeTo: Bundle.main.bundleURL) - return extensionBundle(at: extensionsDirectoryURL) -#endif - } - - static func extensionBundle(at url: URL) -> Bundle { - let extensionURLs: [URL] - do { - extensionURLs = try FileManager.default.contentsOfDirectory(at: url, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - } catch let error { - fatalError("🔵 Failed to get the contents of \(url.absoluteString): \(error.localizedDescription)") - } - - // This should be updated to work well with other extensions - guard let extensionURL = extensionURLs.first else { - fatalError("🔵 Failed to find any system extensions") - } - - guard let extensionBundle = Bundle(url: extensionURL) else { - fatalError("🔵 Failed to create a bundle with URL \(extensionURL.absoluteString)") - } - - return extensionBundle - } - - static let keychainType: KeychainType = { -#if NETP_SYSTEM_EXTENSION - .system -#else - .dataProtection(.named(Bundle.main.appGroup(bundle: .netP))) -#endif - }() -} diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index e1696a7aba..f54236f387 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -22,6 +22,7 @@ import AppKit import Common import Foundation import NetworkProtection +import NetworkProtectionProxy import SwiftUI /// Controller for the Network Protection debug menu. @@ -29,6 +30,10 @@ import SwiftUI @MainActor final class NetworkProtectionDebugMenu: NSMenu { + private let transparentProxySettings = TransparentProxySettings(defaults: .netP) + + // MARK: - Menus + private let environmentMenu = NSMenu() private let preferredServerMenu: NSMenu @@ -39,7 +44,9 @@ final class NetworkProtectionDebugMenu: NSMenu { private let resetToDefaults = NSMenuItem(title: "Reset Settings to defaults", action: #selector(NetworkProtectionDebugMenu.resetSettings)) - private let exclusionsMenu = NSMenu() + private let excludedRoutesMenu = NSMenu() + private let excludeDDGBrowserTrafficFromVPN = NSMenuItem(title: "DDG Browser", action: #selector(toggleExcludeDDGBrowser)) + private let excludeDBPTrafficFromVPN = NSMenuItem(title: "DBP Background Agent", action: #selector(toggleExcludeDBPBackgroundAgent)) private let shouldEnforceRoutesMenuItem = NSMenuItem(title: "Kill Switch (enforceRoutes)", action: #selector(NetworkProtectionDebugMenu.toggleEnforceRoutesAction)) private let shouldIncludeAllNetworksMenuItem = NSMenuItem(title: "includeAllNetworks", action: #selector(NetworkProtectionDebugMenu.toggleIncludeAllNetworks)) @@ -89,7 +96,6 @@ final class NetworkProtectionDebugMenu: NSMenu { .targetting(self) shouldEnforceRoutesMenuItem .targetting(self) - NSMenuItem(title: "Excluded Routes").submenu(exclusionsMenu) NSMenuItem.separator() NSMenuItem(title: "Send Test Notification", action: #selector(NetworkProtectionDebugMenu.sendTestNotification)) @@ -104,6 +110,14 @@ final class NetworkProtectionDebugMenu: NSMenu { NSMenuItem(title: "Environment") .submenu(environmentMenu) + NSMenuItem(title: "Exclusions") { + NSMenuItem(title: "Excluded Apps") { + excludeDDGBrowserTrafficFromVPN.targetting(self) + excludeDBPTrafficFromVPN.targetting(self) + } + NSMenuItem(title: "Excluded Routes").submenu(excludedRoutesMenu) + } + NSMenuItem(title: "Preferred Server").submenu(preferredServerMenu) NSMenuItem(title: "Registration Key") { @@ -172,8 +186,8 @@ final class NetworkProtectionDebugMenu: NSMenu { populateNetworkProtectionServerListMenuItems() populateNetworkProtectionRegistrationKeyValidityMenuItems() - exclusionsMenu.delegate = self - exclusionsMenu.autoenablesItems = false + excludedRoutesMenu.delegate = self + excludedRoutesMenu.autoenablesItems = false populateExclusionsMenuItems() } @@ -391,7 +405,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } private func populateExclusionsMenuItems() { - exclusionsMenu.removeAllItems() + excludedRoutesMenu.removeAllItems() for item in settings.excludedRoutes { let menuItem: NSMenuItem @@ -406,7 +420,7 @@ final class NetworkProtectionDebugMenu: NSMenu { target: self, representedObject: range.stringRepresentation) } - exclusionsMenu.addItem(menuItem) + excludedRoutesMenu.addItem(menuItem) } // Only allow testers to enter a custom code if they're on the waitlist, to simulate the correct path through the flow @@ -419,6 +433,7 @@ final class NetworkProtectionDebugMenu: NSMenu { override func update() { updateEnvironmentMenu() + updateExclusionsMenu() updatePreferredServerMenu() updateRekeyValidityMenu() updateNetworkProtectionMenuItemsState() @@ -588,6 +603,7 @@ final class NetworkProtectionDebugMenu: NSMenu { } // MARK: Environment + @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { let title = menuItem.title let selectedEnvironment: VPNSettings.SelectedEnvironment @@ -608,6 +624,24 @@ final class NetworkProtectionDebugMenu: NSMenu { settings.selectedServer = .automatic } } + + // MARK: - Exclusions + + private let dbpBackgroundAppIdentifier = Bundle.main.dbpBackgroundAgentBundleId + private let ddgBrowserAppIdentifier = Bundle.main.bundleIdentifier! + + private func updateExclusionsMenu() { + excludeDBPTrafficFromVPN.state = transparentProxySettings.isExcluding(dbpBackgroundAppIdentifier) ? .on : .off + excludeDDGBrowserTrafficFromVPN.state = transparentProxySettings.isExcluding(ddgBrowserAppIdentifier) ? .on : .off + } + + @objc private func toggleExcludeDBPBackgroundAgent() { + transparentProxySettings.toggleExclusion(for: dbpBackgroundAppIdentifier) + } + + @objc private func toggleExcludeDDGBrowser() { + transparentProxySettings.toggleExclusion(for: ddgBrowserAppIdentifier) + } } extension NetworkProtectionDebugMenu: NSMenuDelegate { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index f67223a545..0bd4975196 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -24,6 +24,7 @@ import SwiftUI import Common import NetworkExtension import NetworkProtection +import NetworkProtectionProxy import NetworkProtectionUI import Networking import PixelKit @@ -38,6 +39,8 @@ typealias NetworkProtectionConfigChangeHandler = () -> Void final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider { + // MARK: - Settings + let settings: VPNSettings // MARK: - Combine Cancellables @@ -60,6 +63,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// private let controllerErrorStore = NetworkProtectionControllerErrorStore() + private let notificationCenter: NotificationCenter + // MARK: - VPN Tunnel & Configuration /// Auth token store @@ -95,6 +100,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Loads the configuration matching our ``extensionID``. /// + @MainActor public var manager: NETunnelProviderManager? { get async { if let internalManager { @@ -139,13 +145,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr init(networkExtensionBundleID: String, networkExtensionController: NetworkExtensionController, settings: VPNSettings, - notificationCenter: NotificationCenter = .default, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), + notificationCenter: NotificationCenter = .default, logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger()) { self.logger = logger self.networkExtensionBundleID = networkExtensionBundleID self.networkExtensionController = networkExtensionController + self.notificationCenter = notificationCenter self.settings = settings self.tokenStore = tokenStore @@ -254,7 +261,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr tunnelManager.protocolConfiguration = { let protocolConfiguration = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server - protocolConfiguration.providerBundleIdentifier = NetworkProtectionBundle.extensionBundle().bundleIdentifier + protocolConfiguration.providerBundleIdentifier = Bundle.tunnelExtensionBundleID protocolConfiguration.providerConfiguration = [ NetworkProtectionOptionKey.defaultPixelHeaders: APIRequest.Headers().httpHeaders, NetworkProtectionOptionKey.includedRoutes: includedRoutes().map(\.stringRepresentation) as NSArray @@ -304,6 +311,14 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } } + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + // MARK: - Connection Status Querying /// Queries Network Protection to know if its VPN is connected. diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 5770b78a2f..3a3a392736 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -224,7 +224,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: NetworkProtectionBundle.keychainType, + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings) @@ -232,7 +232,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, - keychainType: NetworkProtectionBundle.keychainType, + keychainType: Bundle.keychainType, tokenStore: tokenStore, debugEvents: debugEvents, providerEvents: Self.packetTunnelProviderEvents, @@ -323,13 +323,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { case missingPixelHeaders } - override func prepareToConnect(using provider: NETunnelProviderProtocol?) { - super.prepareToConnect(using: provider) - - guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } - try? loadDefaultPixelHeaders(from: options) - } - public override func loadVendorOptions(from provider: NETunnelProviderProtocol?) throws { try super.loadVendorOptions(from: provider) @@ -350,6 +343,15 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { setupPixels(defaultHeaders: defaultPixelHeaders) } + // MARK: - Overrideable Connection Events + + override func prepareToConnect(using provider: NETunnelProviderProtocol?) { + super.prepareToConnect(using: provider) + + guard PixelKit.shared == nil, let options = provider?.providerConfiguration else { return } + try? loadDefaultPixelHeaders(from: options) + } + // MARK: - Start/Stop Tunnel override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift new file mode 100644 index 0000000000..d300309ec6 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacTransparentProxyProvider.swift @@ -0,0 +1,94 @@ +// +// MacTransparentProxyProvider.swift +// +// 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 Combine +import Common +import Foundation +import Networking +import NetworkExtension +import NetworkProtectionProxy +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit + +final class MacTransparentProxyProvider: TransparentProxyProvider { + + static var vpnProxyLogger = Logger(subsystem: OSLog.subsystem, category: "VPN Proxy") + + private var cancellables = Set() + + @objc init() { + let loadSettingsFromStartupOptions: Bool = { +#if NETP_SYSTEM_EXTENSION + true +#else + false +#endif + }() + + let settings: TransparentProxySettings = { +#if NETP_SYSTEM_EXTENSION + /// Because our System Extension is running in the system context and doesn't have access + /// to shared user defaults, we just make it use the `.standard` defaults. + TransparentProxySettings(defaults: .standard) +#else + /// Because our App Extension is running in the user context and has access + /// to shared user defaults, we take advantage of this and use the `.netP` defaults. + TransparentProxySettings(defaults: .netP) +#endif + }() + + let configuration = TransparentProxyProvider.Configuration( + loadSettingsFromProviderConfiguration: loadSettingsFromStartupOptions) + + super.init(settings: settings, + configuration: configuration, + logger: Self.vpnProxyLogger) + + eventHandler = eventHandler(_:) + +#if !NETP_SYSTEM_EXTENSION + let dryRun: Bool +#if DEBUG + dryRun = true +#else + dryRun = false +#endif + + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: "vpnProxyExtension", + defaultHeaders: [:], + log: .networkProtectionPixel, + defaults: .netP) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequest.Headers(additionalHeaders: headers) + let configuration = APIRequest.Configuration(url: url, method: .get, queryParameters: parameters, headers: apiHeaders) + let request = APIRequest(configuration: configuration) + + request.fetch { _, error in + onComplete(error == nil, error) + } + } +#endif + } + + private func eventHandler(_ event: TransparentProxyProvider.Event) { + PixelKit.fire(event) + } +} diff --git a/DuckDuckGo/NetworkProtectionAppExtension.entitlements b/DuckDuckGo/NetworkProtectionAppExtension.entitlements index 13dd983ca1..d37610bb07 100644 --- a/DuckDuckGo/NetworkProtectionAppExtension.entitlements +++ b/DuckDuckGo/NetworkProtectionAppExtension.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 2c714304df..ee5c9f0c91 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -46,6 +46,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let selectedItem = selectedItem { selectedItemIndex = items.firstIndex(of: selectedItem) + updateTabAudioState(tab: selectedItem) } else { selectedItemIndex = nil } @@ -57,6 +58,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let hoveredItem = hoveredItem { hoveredItemIndex = items.firstIndex(of: hoveredItem) + updateTabAudioState(tab: hoveredItem) } else { hoveredItemIndex = nil } @@ -72,6 +74,7 @@ final class PinnedTabsViewModel: ObservableObject { @Published private(set) var selectedItemIndex: Int? @Published private(set) var hoveredItemIndex: Int? @Published private(set) var dragMovesWindow: Bool = true + @Published private(set) var audioStateView: AudioStateView = .notSupported @Published private(set) var itemsWithoutSeparator: Set = [] @@ -111,6 +114,18 @@ final class PinnedTabsViewModel: ObservableObject { } itemsWithoutSeparator = items } + + private func updateTabAudioState(tab: Tab) { + let audioState = tab.audioState + switch audioState { + case .muted: + audioStateView = .muted + case .unmuted: + audioStateView = .unmuted + case .notSupported: + audioStateView = .notSupported + } + } } // MARK: - Context Menu @@ -124,6 +139,13 @@ extension PinnedTabsViewModel { case fireproof(Tab) case removeFireproofing(Tab) case close(Int) + case muteOrUnmute(Tab) + } + + enum AudioStateView { + case muted + case unmuted + case notSupported } func isFireproof(_ tab: Tab) -> Bool { @@ -168,4 +190,9 @@ extension PinnedTabsViewModel { func removeFireproofing(_ tab: Tab) { contextMenuActionSubject.send(.removeFireproofing(tab)) } + + func muteOrUmute(_ tab: Tab) { + contextMenuActionSubject.send(.muteOrUnmute(tab)) + updateTabAudioState(tab: tab) + } } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 148b0d5d82..278fdfa7ca 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -21,8 +21,8 @@ import SwiftUIExtensions struct PinnedTabView: View { enum Const { - static let dimension: CGFloat = 32 - static let cornerRadius: CGFloat = 6 + static let dimension: CGFloat = 34 + static let cornerRadius: CGFloat = 10 } @ObservedObject var model: Tab @@ -96,7 +96,17 @@ struct PinnedTabView: View { fireproofAction Divider() - + switch collectionModel.audioStateView { + case .muted, .unmuted: + let audioStateText = collectionModel.audioStateView == .muted ? UserText.unmuteTab : UserText.muteTab + Button(audioStateText) { [weak collectionModel, weak model] in + guard let model = model else { return } + collectionModel?.muteOrUmute(model) + } + Divider() + case .notSupported: + EmptyView() + } Button(UserText.closeTab) { [weak collectionModel, weak model] in guard let model = model else { return } collectionModel?.close(model) @@ -163,6 +173,7 @@ struct PinnedTabInnerView: View { var foregroundColor: Color var drawSeparator: Bool = true + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var model: Tab @Environment(\.controlActiveState) private var controlActiveState @@ -187,11 +198,32 @@ struct PinnedTabInnerView: View { .frame(width: PinnedTabView.Const.dimension) } + @ViewBuilder + var mutedTabIndicator: some View { + switch model.audioState { + case .muted: + ZStack { + Circle() + .stroke(Color.gray.opacity(0.5), lineWidth: 0.5) + .background(Circle().foregroundColor(Color("PinnedTabMuteStateCircleColor"))) + .frame(width: 16, height: 16) + Image("Audio-Mute") + .resizable() + .renderingMode(.template) + .frame(width: 12, height: 12) + }.offset(x: 8, y: -8) + default: EmptyView() + } + } + @ViewBuilder var favicon: some View { if let favicon = model.favicon { - Image(nsImage: favicon) - .resizable() + ZStack(alignment: .topTrailing) { + Image(nsImage: favicon) + .resizable() + mutedTabIndicator + } } else if let domain = model.content.url?.host, let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() @@ -199,11 +231,15 @@ struct PinnedTabInnerView: View { Text(firstLetter) .font(.caption) .foregroundColor(.white) + mutedTabIndicator } .cornerRadius(4.0) } else { - Image(nsImage: #imageLiteral(resourceName: "Web")) - .resizable() + ZStack { + Image(nsImage: #imageLiteral(resourceName: "Web")) + .resizable() + mutedTabIndicator + } } } } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index aba8f89859..5c1c11d1db 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -488,6 +488,7 @@ protocol NewWindowPolicyDecisionMaker { } #endif + self.audioState = webView.audioState() addDeallocationChecks(for: webView) } @@ -934,6 +935,14 @@ protocol NewWindowPolicyDecisionMaker { } } + @Published private(set) var audioState: WKWebView.AudioState = .notSupported + + func muteUnmuteTab() { + webView.muteOrUnmute() + + audioState = webView.audioState() + } + @MainActor(unsafe) @discardableResult private func reloadIfNeeded(shouldLoadInBackground: Bool = false) -> ExpectedNavigation? { diff --git a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard index 2bf37ba9b2..ff559dfdfe 100644 --- a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard +++ b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard @@ -1,7 +1,7 @@ - + - + @@ -62,7 +62,7 @@ - + - + @@ -116,7 +125,7 @@ - + @@ -124,7 +133,9 @@ + + @@ -134,11 +145,12 @@ - - + + + @@ -147,7 +159,7 @@ - + @@ -158,17 +170,20 @@ + + + diff --git a/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift new file mode 100644 index 0000000000..85891c6604 --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/BrowserWindowManager.swift @@ -0,0 +1,64 @@ +// +// BrowserWindowManager.swift +// +// 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 AppKit +import Foundation +import WebKit + +/// A class that offers functionality to quickly show an interactive browser window. +/// +/// This class is meant to aid with debugging and should not be included in release builds. +/// . +final class BrowserWindowManager: NSObject { + private var interactiveBrowserWindow: NSWindow? + + @MainActor + func show(domain: String) { + if let interactiveBrowserWindow, interactiveBrowserWindow.isVisible { + return + } + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, defer: false) + window.center() + window.title = "Web Browser" + window.delegate = self + interactiveBrowserWindow = window + + // Create the WKWebView. + let webView = WKWebView(frame: window.contentView!.bounds) + webView.autoresizingMask = [.width, .height] + window.contentView!.addSubview(webView) + + // Load a URL. + let url = URL(string: domain)! + let request = URLRequest(url: url) + webView.load(request) + + // Show the window. + window.makeKeyAndOrderFront(nil) + } +} + +extension BrowserWindowManager: NSWindowDelegate { + func windowWillClose(_ notification: Notification) { + interactiveBrowserWindow = nil + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index 1d7c0403fb..c452200f7b 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -17,10 +17,10 @@ // import Combine -import Foundation +import Common import DataBrokerProtection +import Foundation import PixelKit -import Common /// Manages the IPC service for the Agent app /// @@ -28,6 +28,7 @@ import Common /// demand interaction with. /// final class IPCServiceManager { + private var browserWindowManager: BrowserWindowManager private let ipcServer: DataBrokerProtectionIPCServer private let scheduler: DataBrokerProtectionScheduler private let pixelHandler: EventMapping @@ -41,6 +42,8 @@ final class IPCServiceManager { self.scheduler = scheduler self.pixelHandler = pixelHandler + browserWindowManager = BrowserWindowManager() + ipcServer.serverDelegate = self ipcServer.activate() } @@ -102,4 +105,10 @@ extension IPCServiceManager: IPCServerInterface { pixelHandler.fire(.ipcServerRunAllOperations) scheduler.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + Task { @MainActor in + browserWindowManager.show(domain: domain) + } + } } diff --git a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements index 653311b9ec..2797c3f947 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPN.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPN.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index add4af8a6d..f14bbac165 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -23,7 +23,7 @@ import LoginItems import Networking import NetworkExtension import NetworkProtection -import NetworkProtectionIPC +import NetworkProtectionProxy import NetworkProtectionUI import ServiceManagement import PixelKit @@ -60,18 +60,82 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private var cancellables = Set() - var networkExtensionBundleID: String { - Bundle.main.networkExtensionBundleID + var proxyExtensionBundleID: String { + Bundle.proxyExtensionBundleID } -#if NETWORK_PROTECTION - private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: networkExtensionBundleID) + var tunnelExtensionBundleID: String { + Bundle.tunnelExtensionBundleID + } + + private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: tunnelExtensionBundleID) + + private var storeProxySettingsInProviderConfiguration: Bool { +#if NETP_SYSTEM_EXTENSION + true +#else + false #endif + } private lazy var tunnelSettings = VPNSettings(defaults: .netP) + private lazy var proxySettings = TransparentProxySettings(defaults: .netP) + + @MainActor + private lazy var vpnProxyLauncher = VPNProxyLauncher( + tunnelController: tunnelController, + proxyController: proxyController) + + @MainActor + private lazy var proxyController: TransparentProxyController = { + let controller = TransparentProxyController( + extensionID: proxyExtensionBundleID, + storeSettingsInProviderConfiguration: storeProxySettingsInProviderConfiguration, + settings: proxySettings) { [weak self] manager in + guard let self else { return } + + manager.localizedDescription = "DuckDuckGo VPN Proxy" + + if !manager.isEnabled { + manager.isEnabled = true + } + + manager.protocolConfiguration = { + let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() + protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server + protocolConfiguration.providerBundleIdentifier = self.proxyExtensionBundleID + + // always-on + protocolConfiguration.disconnectOnSleep = false + + // kill switch + // protocolConfiguration.enforceRoutes = false + + // this setting breaks Connection Tester + // protocolConfiguration.includeAllNetworks = settings.includeAllNetworks + + // This is intentionally not used but left here for documentation purposes. + // The reason for this is that we want to have full control of the routes that + // are excluded, so instead of using this setting we're just configuring the + // excluded routes through our VPNSettings class, which our extension reads directly. + // protocolConfiguration.excludeLocalNetworks = settings.excludeLocalNetworks + return protocolConfiguration + }() + } + + controller.eventHandler = handleControllerEvent(_:) + + return controller + }() + + private func handleControllerEvent(_ event: TransparentProxyController.Event) { + PixelKit.fire(event) + } + + @MainActor private lazy var tunnelController = NetworkProtectionTunnelController( - networkExtensionBundleID: networkExtensionBundleID, + networkExtensionBundleID: tunnelExtensionBundleID, networkExtensionController: networkExtensionController, settings: tunnelSettings) @@ -79,6 +143,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { /// /// This is used by our main app to control the tunnel through the VPN login item. /// + @MainActor private lazy var tunnelControllerIPCService: TunnelControllerIPCService = { let ipcServer = TunnelControllerIPCService( tunnelController: tunnelController, @@ -88,17 +153,19 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { return ipcServer }() + @MainActor + private lazy var statusObserver = ConnectionStatusObserverThroughSession( + tunnelSessionProvider: tunnelController, + platformNotificationCenter: NSWorkspace.shared.notificationCenter, + platformDidWakeNotification: NSWorkspace.didWakeNotification) + + @MainActor private lazy var statusReporter: NetworkProtectionStatusReporter = { let errorObserver = ConnectionErrorObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, platformDidWakeNotification: NSWorkspace.didWakeNotification) - let statusObserver = ConnectionStatusObserverThroughSession( - tunnelSessionProvider: tunnelController, - platformNotificationCenter: NSWorkspace.shared.notificationCenter, - platformDidWakeNotification: NSWorkspace.didWakeNotification) - let serverInfoObserver = ConnectionServerInfoObserverThroughSession( tunnelSessionProvider: tunnelController, platformNotificationCenter: NSWorkspace.shared.notificationCenter, @@ -113,6 +180,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { ) }() + @MainActor private lazy var vpnAppEventsHandler = { VPNAppEventsHandler(tunnelController: tunnelController) }() @@ -175,8 +243,9 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { bouncer.requireAuthTokenOrKillApp() - // Initialize the IPC server + // Initialize lazy properties _ = tunnelControllerIPCService + _ = vpnProxyLauncher let dryRun: Bool diff --git a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements index a6ed34f64f..f531d0bc0c 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements +++ b/DuckDuckGoVPN/DuckDuckGoVPNDebug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.developer.system-extension.install diff --git a/DuckDuckGoVPN/Info-AppStore.plist b/DuckDuckGoVPN/Info-AppStore.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info-AppStore.plist +++ b/DuckDuckGoVPN/Info-AppStore.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/Info.plist b/DuckDuckGoVPN/Info.plist index 7627fdd9c9..a1b2b02a02 100644 --- a/DuckDuckGoVPN/Info.plist +++ b/DuckDuckGoVPN/Info.plist @@ -6,8 +6,10 @@ $(DISTRIBUTED_NOTIFICATIONS_PREFIX) NETP_APP_GROUP $(NETP_APP_GROUP) - SYSEX_BUNDLE_ID - $(SYSEX_BUNDLE_ID) + PROXY_EXTENSION_BUNDLE_ID + $(PROXY_EXTENSION_BUNDLE_ID) + TUNNEL_EXTENSION_BUNDLE_ID + $(TUNNEL_EXTENSION_BUNDLE_ID) LSApplicationCategoryType public.app-category.productivity CFBundleShortVersionString diff --git a/DuckDuckGoVPN/VPNProxyLauncher.swift b/DuckDuckGoVPN/VPNProxyLauncher.swift new file mode 100644 index 0000000000..c99d187cf2 --- /dev/null +++ b/DuckDuckGoVPN/VPNProxyLauncher.swift @@ -0,0 +1,149 @@ +// +// VPNProxyLauncher.swift +// +// 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 Combine +import Foundation +import NetworkProtectionProxy +import NetworkExtension + +/// Starts and stops the VPN proxy component. +/// +/// This class looks at the tunnel and the proxy components and their status and settings, and decides based on +/// a number of conditions whether to start the proxy, stop it, or just leave it be. +/// +@MainActor +final class VPNProxyLauncher { + private let tunnelController: NetworkProtectionTunnelController + private let proxyController: TransparentProxyController + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + init(tunnelController: NetworkProtectionTunnelController, + proxyController: TransparentProxyController, + notificationCenter: NotificationCenter = .default) { + + self.notificationCenter = notificationCenter + self.proxyController = proxyController + self.tunnelController = tunnelController + + subscribeToStatusChanges() + subscribeToProxySettingChanges() + } + + // MARK: - Status Changes + + private func subscribeToStatusChanges() { + notificationCenter.publisher(for: .NEVPNStatusDidChange) + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink(receiveValue: statusChanged(notification:)) + .store(in: &cancellables) + } + + private func statusChanged(notification: Notification) { + Task { @MainActor in + let isProxyConnectionStatusChange = await proxyController.connection == notification.object as? NEVPNConnection + + try await startOrStopProxyIfNeeded(isProxyConnectionStatusChange: isProxyConnectionStatusChange) + } + } + + // MARK: - Proxy Settings Changes + + private func subscribeToProxySettingChanges() { + proxyController.settings.changePublisher + .sink(receiveValue: proxySettingChanged(_:)) + .store(in: &cancellables) + } + + private func proxySettingChanged(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + try await startOrStopProxyIfNeeded() + } + } + + // MARK: - Auto starting & stopping the proxy component + + private var isControllingProxy = false + + private func startOrStopProxyIfNeeded(isProxyConnectionStatusChange: Bool = false) async throws { + if await shouldStartProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + + // When we're auto-starting the proxy because its own status changed to + // disconnected, we want to give it a pause because if it fails to connect again + // we risk the proxy entering a frenetic connect / disconnect loop + if isProxyConnectionStatusChange { + // If the proxy connection was stopped, let's wait a bit before trying to enable it again + try await Task.sleep(interval: .seconds(10)) + + // And we want to check again if the proxy still needs to start after waiting + guard await shouldStartProxy else { + return + } + } + + do { + try await proxyController.start() + isControllingProxy = false + } catch { + isControllingProxy = false + throw error + } + } else if await shouldStopProxy { + guard !isControllingProxy else { + return + } + + isControllingProxy = true + await proxyController.stop() + isControllingProxy = false + } + } + + private var shouldStartProxy: Bool { + get async { + let proxyIsDisconnected = await proxyController.status == .disconnected + let tunnelIsConnected = await tunnelController.status == .connected + + // Starting the proxy only when it's required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsDisconnected + && tunnelIsConnected + && proxyController.isRequiredForActiveFeatures + } + } + + private var shouldStopProxy: Bool { + get async { + let proxyIsConnected = await proxyController.status == .connected + let tunnelIsDisconnected = await tunnelController.status == .disconnected + + // Stopping the proxy when it's not required for active features + // is a product decision. It may change once we decide the proxy + // is stable enough to be running at all times. + return proxyIsConnected + && (tunnelIsDisconnected || !proxyController.isRequiredForActiveFeatures) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index fb62529ee7..e8853bdc23 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index 857f174360..52eabbae92 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -290,4 +290,26 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { return mapper.maintenanceScanState(brokerProfileQueryData) } + + func getDataBrokers() async -> [DBPUIDataBroker] { + brokerProfileQueryData + // 1. We get all brokers (in this list brokers are repeated) + .map { $0.dataBroker } + // 2. We map the brokers to the UI model + .flatMap { dataBroker -> [DBPUIDataBroker] in + var result: [DBPUIDataBroker] = [] + result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url)) + + for mirrorSite in dataBroker.mirrorSites { + result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url)) + } + return result + } + // 3. We delete duplicates + .reduce(into: [DBPUIDataBroker]()) { (result, dataBroker) in + if !result.contains(where: { $0.url == dataBroker.url }) { + result.append(dataBroker) + } + } + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift index 18b5ffa6b6..fd09e77417 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserView.swift @@ -49,6 +49,7 @@ struct DatabaseView: View { @State private var isPopoverVisible = false @State private var selectedData: String = "" let data: [DataBrokerDatabaseBrowserData.Row] + let rowHeight: CGFloat = 40.0 var body: some View { if data.count > 0 { @@ -62,6 +63,11 @@ struct DatabaseView: View { } } + private func spacerHeight(_ geometry: GeometryProxy) -> CGFloat { + let result = geometry.size.height - CGFloat(data.count) * rowHeight + return max(0, result) + } + private func dataView() -> some View { GeometryReader { geometry in ScrollView([.horizontal, .vertical]) { @@ -86,7 +92,8 @@ struct DatabaseView: View { ForEach(row.data.keys.sorted(), id: \.self) { key in VStack { Text("\(row.data[key]?.description ?? "")") - .frame(maxWidth: 200, maxHeight: 50) + .frame(maxWidth: 200) + .frame(height: rowHeight) .frame(minWidth: 60) .onTapGesture { selectedData = row.data[key]?.description ?? "" @@ -100,7 +107,8 @@ struct DatabaseView: View { } } } - Spacer(minLength: geometry.size.height) + Spacer() + .frame(height: spacerHeight(geometry)) } .frame(minWidth: geometry.size.width, minHeight: 0, alignment: .topLeading) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 4a6f58ac28..2aa3953437 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -17,9 +17,9 @@ // import Combine +import Common import Foundation import XPCHelper -import Common /// This protocol describes the server-side IPC interface for controlling the tunnel /// @@ -150,6 +150,17 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { // If you add a completion block, please remember to call it here too! }) } + + public func openBrowser(domain: String) { + self.pixelHandler.fire(.ipcServerRunAllOperations) + xpc.execute(call: { server in + server.openBrowser(domain: domain) + }, xpcReplyErrorHandler: { error in + os_log("Error \(error.localizedDescription)") + // Intentional no-op as there's no completion block + // If you add a completion block, please remember to call it here too! + }) + } } // MARK: - Incoming communication from the server diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index fde0274a5f..a2bc3d0e56 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -42,6 +42,12 @@ public protocol IPCServerInterface: AnyObject { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } /// This protocol describes the server-side XPC interface. @@ -71,6 +77,12 @@ protocol XPCServerInterface { func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) func runAllOperations(showWebView: Bool) + + // MARK: - Debugging Features + + /// Opens a browser window with the specified domain + /// + func openBrowser(domain: String) } public final class DataBrokerProtectionIPCServer { @@ -146,4 +158,8 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { func runAllOperations(showWebView: Bool) { serverDelegate?.runAllOperations(showWebView: showWebView) } + + func openBrowser(domain: String) { + serverDelegate?.openBrowser(domain: domain) + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 353e3912df..5f242b34a2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -103,12 +103,24 @@ struct DBPUIAddressAtIndex: Codable { /// Message Object representing a data broker struct DBPUIDataBroker: Codable, Hashable { let name: String + let url: String + let date: Double? + + init(name: String, url: String, date: Double? = nil) { + self.name = name + self.url = url + self.date = date + } func hash(into hasher: inout Hasher) { hasher.combine(name) } } +struct DBPUIDataBrokerList: DBPUISendableMessage { + let dataBrokers: [DBPUIDataBroker] +} + /// Message Object representing a requested change to the user profile's brith year struct DBPUIBirthYear: Codable { let year: Int @@ -123,6 +135,7 @@ struct DBPUIDataBrokerProfileMatch: Codable { let addresses: [DBPUIUserProfileAddress] let alternativeNames: [String] let relatives: [String] + let date: Double? // Used in some methods to set the removedDate or found date } /// Protocol to represent a message that can be passed from the host to the UI @@ -139,6 +152,10 @@ struct DBPUIScanAndOptOutMaintenanceState: DBPUISendableMessage { struct DBPUIOptOutMatch: DBPUISendableMessage { let dataBroker: DBPUIDataBroker let matches: Int + let name: String + let alternativeNames: [String] + let addresses: [DBPUIUserProfileAddress] + let date: Double } /// Data representing the initial scan progress diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift index fb96638206..e1a17f590b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift @@ -32,13 +32,46 @@ extension Int { struct MirrorSite: Codable, Sendable { let name: String + let url: String let addedAt: Date let removedAt: Date? + + enum CodingKeys: CodingKey { + case name + case url + case addedAt + case removedAt + } + + init(name: String, url: String, addedAt: Date, removedAt: Date? = nil) { + self.name = name + self.url = url + self.addedAt = addedAt + self.removedAt = removedAt + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + + // The older versions of the JSON file did not have a URL property. + // When decoding those cases, we fallback to its name, since the name was the URL. + do { + url = try container.decode(String.self, forKey: .url) + } catch { + url = name + } + + addedAt = try container.decode(Date.self, forKey: .addedAt) + removedAt = try? container.decode(Date.self, forKey: .removedAt) + + } } struct DataBroker: Codable, Sendable { let id: Int64? let name: String + let url: String let steps: [Step] let version: String let schedulingConfig: DataBrokerScheduleConfig @@ -51,6 +84,7 @@ struct DataBroker: Codable, Sendable { enum CodingKeys: CodingKey { case name + case url case steps case version case schedulingConfig @@ -60,6 +94,7 @@ struct DataBroker: Codable, Sendable { init(id: Int64? = nil, name: String, + url: String, steps: [Step], version: String, schedulingConfig: DataBrokerScheduleConfig, @@ -68,6 +103,13 @@ struct DataBroker: Codable, Sendable { ) { self.id = id self.name = name + + if url.isEmpty { + self.url = name + } else { + self.url = url + } + self.steps = steps self.version = version self.schedulingConfig = schedulingConfig @@ -78,6 +120,15 @@ struct DataBroker: Codable, Sendable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) + + // The older versions of the JSON file did not have a URL property. + // When decoding those cases, we fallback to its name, since the name was the URL. + do { + url = try container.decode(String.self, forKey: .url) + } catch { + url = name + } + version = try container.decode(String.self, forKey: .version) steps = try container.decode([Step].self, forKey: .steps) schedulingConfig = try container.decode(DataBrokerScheduleConfig.self, forKey: .schedulingConfig) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift index 29757bc891..70a76305b3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift @@ -77,6 +77,7 @@ struct ExtractedProfile: Codable, Sendable { var email: String? var removedDate: Date? let fullName: String? + let identifier: String? enum CodingKeys: CodingKey { case id @@ -92,6 +93,7 @@ struct ExtractedProfile: Codable, Sendable { case email case removedDate case fullName + case identifier } init(id: Int64? = nil, @@ -105,7 +107,8 @@ struct ExtractedProfile: Codable, Sendable { reportId: String? = nil, age: String? = nil, email: String? = nil, - removedDate: Date? = nil) { + removedDate: Date? = nil, + identifier: String? = nil) { self.id = id self.name = name self.alternativeNames = alternativeNames @@ -119,6 +122,29 @@ struct ExtractedProfile: Codable, Sendable { self.email = email self.removedDate = removedDate self.fullName = name + self.identifier = identifier + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decodeIfPresent(Int64.self, forKey: .id) + name = try container.decodeIfPresent(String.self, forKey: .name) + alternativeNames = try container.decodeIfPresent([String].self, forKey: .alternativeNames) + addressFull = try container.decodeIfPresent(String.self, forKey: .addressFull) + addresses = try container.decodeIfPresent([AddressCityState].self, forKey: .addresses) + phoneNumbers = try container.decodeIfPresent([String].self, forKey: .phoneNumbers) + relatives = try container.decodeIfPresent([String].self, forKey: .relatives) + profileUrl = try container.decodeIfPresent(String.self, forKey: .profileUrl) + reportId = try container.decodeIfPresent(String.self, forKey: .reportId) + age = try container.decodeIfPresent(String.self, forKey: .age) + email = try container.decodeIfPresent(String.self, forKey: .email) + removedDate = try container.decodeIfPresent(Date.self, forKey: .removedDate) + fullName = try container.decodeIfPresent(String.self, forKey: .fullName) + if let identifier = try container.decodeIfPresent(String.self, forKey: .identifier) { + self.identifier = identifier + } else { + self.identifier = profileUrl + } } func merge(with profile: ProfileQuery) -> ExtractedProfile { @@ -134,7 +160,8 @@ struct ExtractedProfile: Codable, Sendable { reportId: self.reportId, age: self.age ?? String(profile.age), email: self.email, - removedDate: self.removedDate + removedDate: self.removedDate, + identifier: self.identifier ) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 5777596838..37cd385e4e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -99,7 +99,7 @@ extension DataBrokerOperation { if action.needsEmail { do { stageCalculator?.setStage(.emailGenerate) - extractedProfile?.email = try await emailService.getEmail(dataBrokerName: query.dataBroker.name) + extractedProfile?.email = try await emailService.getEmail(dataBrokerURL: query.dataBroker.url) stageCalculator?.fireOptOutEmailGenerate() } catch { await onError(error: DataBrokerProtectionError.emailError(error as? EmailError)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 1b043a6d41..73f08604f6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -135,10 +135,10 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // We check if the profile exists in the database. let extractedProfilesForBroker = database.fetchExtractedProfiles(for: brokerId) - let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.profileUrl == extractedProfile.profileUrl } + let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.identifier == extractedProfile.identifier } // If the profile exists we do not create a new opt-out operation - if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.profileUrl == extractedProfile.profileUrl }), let id = alreadyInDatabaseProfile.id { + if doesProfileExistsInDatabase, let alreadyInDatabaseProfile = extractedProfilesForBroker.first(where: { $0.identifier == extractedProfile.identifier }), let id = alreadyInDatabaseProfile.id { // If it was removed in the past but was found again when scanning, it means it appearead again, so we reset the remove date. if alreadyInDatabaseProfile.removedDate != nil { database.updateRemovedDate(nil, on: id) @@ -176,7 +176,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { // Check for removed profiles let removedProfiles = brokerProfileQueryData.extractedProfiles.filter { savedProfile in !extractedProfiles.contains { recentlyFoundProfile in - recentlyFoundProfile.profileUrl == savedProfile.profileUrl + recentlyFoundProfile.identifier == savedProfile.identifier } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 28c2128a00..f3203334e2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -152,7 +152,7 @@ public struct DataBrokerProtectionBrokerUpdater { // 2. If does exist, we check the number version, if the version number is new, we update it // 3. If it does not exist, we add it, and we create the scan operations related to it private func update(_ broker: DataBroker) throws { - guard let savedBroker = try vault.fetchBroker(with: broker.name) else { + guard let savedBroker = try vault.fetchBroker(with: broker.url) else { // The broker does not exist in the current storage. We need to add it. try add(broker) return diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift new file mode 100644 index 0000000000..f00192e769 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift @@ -0,0 +1,174 @@ +// +// DataBrokerProtectionEngagementPixels.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 +import Common +import BrowserServicesKit +import PixelKit + +protocol DataBrokerProtectionEngagementPixelsRepository { + func markDailyPixelSent() + func markWeeklyPixelSent() + func markMonthlyPixelSent() + + func getLatestDailyPixel() -> Date? + func getLatestWeeklyPixel() -> Date? + func getLatestMonthlyPixel() -> Date? +} + +final class DataBrokerProtectionEngagementPixelsUserDefaults: DataBrokerProtectionEngagementPixelsRepository { + + enum Consts { + static let dailyPixelKey = "macos.browser.data-broker-protection.dailyPixelKey" + static let weeklyPixelKey = "macos.browser.data-broker-protection.weeklyPixelKey" + static let monthlyPixelKey = "macos.browser.data-broker-protection.monthlyPixelKey" + } + + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + func markDailyPixelSent() { + userDefaults.set(Date(), forKey: Consts.dailyPixelKey) + } + + func markWeeklyPixelSent() { + userDefaults.set(Date(), forKey: Consts.weeklyPixelKey) + } + + func markMonthlyPixelSent() { + userDefaults.set(Date(), forKey: Consts.monthlyPixelKey) + } + + func getLatestDailyPixel() -> Date? { + userDefaults.object(forKey: Consts.dailyPixelKey) as? Date + } + + func getLatestWeeklyPixel() -> Date? { + userDefaults.object(forKey: Consts.weeklyPixelKey) as? Date + } + + func getLatestMonthlyPixel() -> Date? { + userDefaults.object(forKey: Consts.monthlyPixelKey) as? Date + } + +} + +/* + https://app.asana.com/0/1204586965688315/1206648312655381/f + + 1. When a user becomes an "Active User" of your feature, immediately fire individual pixels to register a DAU, a WAU and a MAU. Record (on-device) the date that the pixel was fired for each of the three events. e.g. + - DAU Pixel Last Sent 2024-02-20 + - WAU Pixel Last Sent 2024-02-20 + - MAU Pixel Last Sent 2024-02-20 + 2. If it is >= 1 date since the DAU pixel was last sent, send a new DAU pixel, and update the date with the current date + - DAU Pixel Last Sent 2024-02-21 + - WAU Pixel Last Sent 2024-02-20 + - MAU Pixel Last Sent 2024-02-20 + 3. If it is >= 7 dates since the WAU pixel was last sent, send a new WAU pixel and update the date with the current date + - DAU Pixel Last Sent 2024-02-27 + - WAU Pixel Last Sent 2024-02-27 + - MAU Pixel Last Sent 2024-02-20 + 4. If it is >= 28 dates since the MAU pixel was last sent, send a new MAU pixel and update the date with the current date: + - DAU Pixel Last Sent 2024-03-19 + - WAU Pixel Last Sent 2024-03-19 + - MAU Pixel Last Sent 2024-03-19 + */ +final class DataBrokerProtectionEngagementPixels { + + enum ActiveUserFrequency: Int { + case daily = 1 + case weekly = 7 + case monthly = 28 + } + + private let calendar = Calendar.current + private let database: DataBrokerProtectionRepository + private let repository: DataBrokerProtectionEngagementPixelsRepository + private let handler: EventMapping + + init(database: DataBrokerProtectionRepository, + handler: EventMapping, + repository: DataBrokerProtectionEngagementPixelsRepository = DataBrokerProtectionEngagementPixelsUserDefaults()) { + self.database = database + self.handler = handler + self.repository = repository + } + + func fireEngagementPixel(currentDate: Date = Date()) { + guard database.fetchProfile() != nil else { + os_log("No profile. We do not fire any pixel because we do not consider it an engaged user.", log: .dataBrokerProtection) + return + } + + if shouldWeFireDailyPixel(date: currentDate) { + handler.fire(.dailyActiveUser) + repository.markDailyPixelSent() + } + + if shouldWeFireWeeklyPixel(date: currentDate) { + handler.fire(.weeklyActiveUser) + repository.markWeeklyPixelSent() + } + + if shouldWeFireMonthlyPixel(date: currentDate) { + handler.fire(.monthlyActiveUser) + repository.markMonthlyPixelSent() + } + } + + private func shouldWeFireDailyPixel(date: Date) -> Bool { + guard let latestPixelFire = repository.getLatestDailyPixel() else { + return true + } + + return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .daily) + } + + private func shouldWeFireWeeklyPixel(date: Date) -> Bool { + guard let latestPixelFire = repository.getLatestWeeklyPixel() else { + return true + } + + return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .weekly) + } + + private func shouldWeFireMonthlyPixel(date: Date) -> Bool { + guard let latestPixelFire = repository.getLatestMonthlyPixel() else { + return true + } + + return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .monthly) + } + + private func shouldWeFirePixel(startDate: Date, endDate: Date, daysDifference: ActiveUserFrequency) -> Bool { + if let differenceBetweenDates = differenceBetweenDates(startDate: startDate, endDate: endDate) { + return differenceBetweenDates >= daysDifference.rawValue + } + + return false + } + + private func differenceBetweenDates(startDate: Date, endDate: Date) -> Int? { + let components = calendar.dateComponents([.day], from: startDate, to: endDate) + + return components.day + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index ad3dfda484..a791dd55ee 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -39,168 +39,6 @@ enum ErrorCategory: Equatable { } } -enum Stage: String { - case start - case emailGenerate = "email-generate" - case captchaParse = "captcha-parse" - case captchaSend = "captcha-send" - case captchaSolve = "captcha-solve" - case submit - case emailReceive = "email-receive" - case emailConfirm = "email-confirm" - case validate - case other -} - -protocol StageDurationCalculator { - func durationSinceLastStage() -> Double - func durationSinceStartTime() -> Double - func fireOptOutStart() - func fireOptOutEmailGenerate() - func fireOptOutCaptchaParse() - func fireOptOutCaptchaSend() - func fireOptOutCaptchaSolve() - func fireOptOutSubmit() - func fireOptOutEmailReceive() - func fireOptOutEmailConfirm() - func fireOptOutValidate() - func fireOptOutSubmitSuccess() - func fireOptOutFailure() - func fireScanSuccess(matchesFound: Int) - func fireScanFailed() - func fireScanError(error: Error) - func setStage(_ stage: Stage) -} - -final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { - let handler: EventMapping - let attemptId: UUID - let dataBroker: String - let startTime: Date - var lastStateTime: Date - var stage: Stage = .other - - init(attemptId: UUID = UUID(), - startTime: Date = Date(), - dataBroker: String, - handler: EventMapping) { - self.attemptId = attemptId - self.startTime = startTime - self.lastStateTime = startTime - self.dataBroker = dataBroker - self.handler = handler - } - - /// Returned in milliseconds - func durationSinceLastStage() -> Double { - let now = Date() - let durationSinceLastStage = now.timeIntervalSince(lastStateTime) * 1000 - self.lastStateTime = now - - return durationSinceLastStage.rounded(.towardZero) - } - - /// Returned in milliseconds - func durationSinceStartTime() -> Double { - let now = Date() - return (now.timeIntervalSince(startTime) * 1000).rounded(.towardZero) - } - - func fireOptOutStart() { - setStage(.start) - handler.fire(.optOutStart(dataBroker: dataBroker, attemptId: attemptId)) - } - - func fireOptOutEmailGenerate() { - handler.fire(.optOutEmailGenerate(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutCaptchaParse() { - handler.fire(.optOutCaptchaParse(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutCaptchaSend() { - handler.fire(.optOutCaptchaSend(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutCaptchaSolve() { - handler.fire(.optOutCaptchaSolve(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutSubmit() { - setStage(.submit) - handler.fire(.optOutSubmit(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutEmailReceive() { - handler.fire(.optOutEmailReceive(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutEmailConfirm() { - handler.fire(.optOutEmailConfirm(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutValidate() { - setStage(.validate) - handler.fire(.optOutValidate(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutSubmitSuccess() { - handler.fire(.optOutSubmitSuccess(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) - } - - func fireOptOutFailure() { - handler.fire(.optOutFailure(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceStartTime(), stage: stage.rawValue)) - } - - func fireScanSuccess(matchesFound: Int) { - handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1)) - } - - func fireScanFailed() { - handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1)) - } - - func fireScanError(error: Error) { - var errorCategory: ErrorCategory = .unclassified - - if let dataBrokerProtectionError = error as? DataBrokerProtectionError { - switch dataBrokerProtectionError { - case .httpError(let httpCode): - if httpCode < 500 { - errorCategory = .clientError(httpCode: httpCode) - } else { - errorCategory = .serverError(httpCode: httpCode) - } - default: - errorCategory = .validationError - } - } else { - if let nsError = error as NSError? { - if nsError.domain == NSURLErrorDomain { - errorCategory = .networkError - } - } - } - - handler.fire( - .scanError( - dataBroker: dataBroker, - duration: durationSinceStartTime(), - category: errorCategory.toString, - details: error.localizedDescription - ) - ) - } - - // Helper methods to set the stage that is about to run. This help us - // identifying the stage so we can know which one was the one that failed. - - func setStage(_ stage: Stage) { - self.stage = stage - } -} - public enum DataBrokerProtectionPixels { struct Consts { static let dataBrokerParamKey = "data_broker" @@ -276,10 +114,14 @@ public enum DataBrokerProtectionPixels { case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int) case scanFailed(dataBroker: String, duration: Double, tries: Int) case scanError(dataBroker: String, duration: Double, category: String, details: String) + + // KPIs - engagement + case dailyActiveUser + case weeklyActiveUser + case monthlyActiveUser } extension DataBrokerProtectionPixels: PixelKitEvent { - public var name: String { switch self { case .parentChildMatches: return "m_mac_dbp_macos_parent-child-broker-matches" @@ -332,7 +174,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .disableLoginItem: return "m_mac_dbp_login-item_disable" case .resetLoginItem: return "m_mac_dbp_login-item_reset" - // User Notifications + // User Notifications case .dataBrokerProtectionNotificationSentFirstScanComplete: return "m_mac_dbp_notification_sent_first_scan_complete" case .dataBrokerProtectionNotificationOpenedFirstScanComplete: @@ -349,6 +191,11 @@ extension DataBrokerProtectionPixels: PixelKitEvent { return "m_mac_dbp_notification_sent_all_records_removed" case .dataBrokerProtectionNotificationOpenedAllRecordsRemoved: return "m_mac_dbp_notification_opened_all_records_removed" + + // KPIs - engagement + case .dailyActiveUser: return "m_mac_dbp_engagement_dau" + case .weeklyActiveUser: return "m_mac_dbp_engagement_wau" + case .monthlyActiveUser: return "m_mac_dbp_engagement_mau" } } @@ -411,7 +258,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .dataBrokerProtectionNotificationScheduled2WeeksCheckIn, .dataBrokerProtectionNotificationOpened2WeeksCheckIn, .dataBrokerProtectionNotificationSentAllRecordsRemoved, - .dataBrokerProtectionNotificationOpenedAllRecordsRemoved: + .dataBrokerProtectionNotificationOpenedAllRecordsRemoved, + .dailyActiveUser, + .weeklyActiveUser, + .monthlyActiveUser: return [:] case .ipcServerRegister, .ipcServerStartScheduler, @@ -486,7 +336,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double + func durationSinceStartTime() -> Double + func fireOptOutStart() + func fireOptOutEmailGenerate() + func fireOptOutCaptchaParse() + func fireOptOutCaptchaSend() + func fireOptOutCaptchaSolve() + func fireOptOutSubmit() + func fireOptOutEmailReceive() + func fireOptOutEmailConfirm() + func fireOptOutValidate() + func fireOptOutSubmitSuccess() + func fireOptOutFailure() + func fireScanSuccess(matchesFound: Int) + func fireScanFailed() + func fireScanError(error: Error) + func setStage(_ stage: Stage) +} + +final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { + let handler: EventMapping + let attemptId: UUID + let dataBroker: String + let startTime: Date + var lastStateTime: Date + var stage: Stage = .other + + init(attemptId: UUID = UUID(), + startTime: Date = Date(), + dataBroker: String, + handler: EventMapping) { + self.attemptId = attemptId + self.startTime = startTime + self.lastStateTime = startTime + self.dataBroker = dataBroker + self.handler = handler + } + + /// Returned in milliseconds + func durationSinceLastStage() -> Double { + let now = Date() + let durationSinceLastStage = now.timeIntervalSince(lastStateTime) * 1000 + self.lastStateTime = now + + return durationSinceLastStage.rounded(.towardZero) + } + + /// Returned in milliseconds + func durationSinceStartTime() -> Double { + let now = Date() + return (now.timeIntervalSince(startTime) * 1000).rounded(.towardZero) + } + + func fireOptOutStart() { + setStage(.start) + handler.fire(.optOutStart(dataBroker: dataBroker, attemptId: attemptId)) + } + + func fireOptOutEmailGenerate() { + handler.fire(.optOutEmailGenerate(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutCaptchaParse() { + handler.fire(.optOutCaptchaParse(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutCaptchaSend() { + handler.fire(.optOutCaptchaSend(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutCaptchaSolve() { + handler.fire(.optOutCaptchaSolve(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutSubmit() { + setStage(.submit) + handler.fire(.optOutSubmit(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutEmailReceive() { + handler.fire(.optOutEmailReceive(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutEmailConfirm() { + handler.fire(.optOutEmailConfirm(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutValidate() { + setStage(.validate) + handler.fire(.optOutValidate(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutSubmitSuccess() { + handler.fire(.optOutSubmitSuccess(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage())) + } + + func fireOptOutFailure() { + handler.fire(.optOutFailure(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceStartTime(), stage: stage.rawValue)) + } + + func fireScanSuccess(matchesFound: Int) { + handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1)) + } + + func fireScanFailed() { + handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1)) + } + + func fireScanError(error: Error) { + var errorCategory: ErrorCategory = .unclassified + + if let dataBrokerProtectionError = error as? DataBrokerProtectionError { + switch dataBrokerProtectionError { + case .httpError(let httpCode): + if httpCode < 500 { + errorCategory = .clientError(httpCode: httpCode) + } else { + errorCategory = .serverError(httpCode: httpCode) + } + default: + errorCategory = .validationError + } + } else { + if let nsError = error as NSError? { + if nsError.domain == NSURLErrorDomain { + errorCategory = .networkError + } + } + } + + handler.fire( + .scanError( + dataBroker: dataBroker, + duration: durationSinceStartTime(), + category: errorCategory.toString, + details: error.localizedDescription + ) + ) + } + + // Helper methods to set the stage that is about to run. This help us + // identifying the stage so we can know which one was the one that failed. + + func setStage(_ stage: Stage) { + self.stage = stage + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json index 60823c78ac..d7daac5036 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/advancedbackgroundchecks.com.json @@ -1,8 +1,9 @@ { - "name": "advancedbackgroundchecks.com", - "version": "0.1.0", + "name": "AdvancedBackgroundChecks", + "url": "advancedbackgroundchecks.com", + "version": "0.1.5", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678060800000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "ef8031e6-5e61-4183-b57e-7df156c7129a", + "id": "7967f064-e3c5-442d-8380-99cf752fb8df", "url": "https://www.advancedbackgroundchecks.com/names/${firstName}-${lastName}_${city}-${state}_age_${age}" }, { "actionType": "extract", - "id": "f3ed744c-6cfc-4a99-b46e-6095587eadfc", + "id": "6f6bb616-a4cb-4231-9abb-522722208f95", "selector": ".card-block", "profile": { "name": { @@ -24,7 +25,8 @@ }, "alternativeNamesList": { "selector": "(.//p[@class='card-text max-lines-1'])[1]", - "afterText": "AKA:" + "afterText": "AKA:", + "separator": "," }, "age": { "selector": ".card-title", @@ -39,7 +41,8 @@ }, "relativesList": { "selector": "(.//p[@class='card-text max-lines-1'])[2]", - "afterText": "Related to:" + "afterText": "Related to:", + "separator": "," }, "profileUrl": { "selector": ".link-to-details" @@ -59,4 +62,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json index 5b3800cfcf..552923ca1f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/backgroundcheck.run.json @@ -1,6 +1,7 @@ { "name": "backgroundcheck.run", - "version": "0.1.1", + "url": "backgroundcheck.run", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1677736800000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "aa12b430-8e5d-4c64-bb77-2961f19a1bc8", + "id": "5f90e39f-cb94-4b8d-94ed-48ba0060dc08", "url": "https://backgroundcheck.run/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}" }, { "actionType": "extract", - "id": "75fd2e16-d84a-4bbe-9cf1-79c6d1cc4dec", + "id": "3225fa15-4e00-4e6a-bfc7-a85dfb504c86", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json index 130c996369..7050bab137 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/centeda.com.json @@ -1,8 +1,9 @@ { - "name": "centeda.com", - "version": "0.1.1", + "name": "Centeda", + "url": "centeda.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677715200000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "25990359-3d58-45de-bdfd-d524b1946e57", + "id": "af9c9f03-e778-4c29-85fc-e5cbbfec563c", "url": "https://centeda.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,8 +25,8 @@ }, { "actionType": "extract", - "id": "7108af78-dbbf-47ec-8bb9-e44be505993e", - "selector": ".search-item", + "id": "79fa2a1c-65b4-417a-a8ac-2ca6d729ffc1", + "selector": ".search-result > a", "profile": { "name": { "selector": ".title", @@ -46,7 +47,7 @@ "selector": ".//div[@class='col-sm-24 col-md-8 related-to']//li" }, "profileUrl": { - "selector": ".get-report-btn" + "selector": "a" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json index f871133c15..8b6801fc48 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clubset.com.json @@ -1,6 +1,7 @@ { - "name": "clubset.com", - "version": "0.1.1", + "name": "Clubset", + "url": "clubset.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "917f5d40-2011-4fe5-9ef6-136d6bfaea35", + "id": "5c559c67-c13c-4055-a318-6ba35d62a2cf", "url": "https://clubset.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state|upcase}&city=${city|capitalize}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "06e37215-ef34-4971-bf86-e5a03dfe46e8", + "id": "866bdfc5-069e-4734-9ce0-a19976fa796b", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json index 0aca895c02..4c2bd20999 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/clustrmaps.com.json @@ -1,8 +1,9 @@ { - "name": "clustrmaps.com", - "version": "0.1.1", + "name": "ClustrMaps", + "url": "clustrmaps.com", + "version": "0.1.4", "parent": "neighbor.report", - "addedDatetime": 1692590400000, + "addedDatetime": 1692594000000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "a39655de-5c23-477d-9887-1d34966a1069", + "id": "e6929e37-4764-450a-be2a-73479f11842a", "url": "https://clustrmaps.com/persons/${firstName}-${lastName}/${state|stateFull|capitalize}/${city|hyphenated}" }, { "actionType": "extract", - "id": "4e3a628e-3634-4a2b-b632-4fbb8ce0b52b", + "id": "06f39df7-89c2-40da-b288-cdf3ed0e4bfd", "selector": ".//div[@itemprop='Person']", "profile": { "name": { @@ -55,4 +56,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json index d68f6b9f4c..3df8d7f195 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/councilon.com.json @@ -1,6 +1,7 @@ { - "name": "councilon.com", - "version": "0.1.1", + "name": "Councilon", + "url": "councilon.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "295418e5-e1da-43b4-af50-75576ca4f843", + "id": "a5052dda-d4e7-4d3f-97bc-ef9f0aa9ae5f", "url": "https://councilon.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "eead1b72-7d6b-4cdc-988d-5ea66eb398f1", + "id": "55a50a37-9b1b-40fa-8533-af1273a26258", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json index 33f27d0c79..3b59ed585e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/curadvisor.com.json @@ -1,8 +1,9 @@ { - "name": "curadvisor.com", - "version": "0.1.1", + "name": "CurAdvisor", + "url": "curadvisor.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677736800000, + "addedDatetime": 1703052000000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "bdb69f52-8ece-4d65-9b78-543fef0e90ae", + "id": "ab5503c7-bd11-4320-b38e-c637b239182e", "url": "https://curadvisor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "3b9bc992-ecc0-4dc5-b716-fcea021cbcdb", + "id": "d273c1cf-2635-40d7-b26f-6f34467282cf", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json index e27b744790..f930834db9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/cyberbackgroundchecks.com.json @@ -1,6 +1,7 @@ { - "name": "cyberbackgroundchecks.com", - "version": "0.1.1", + "name": "Cyber Background Checks", + "url": "cyberbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1705644000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "4b7037f3-9c6a-42b5-929e-621256e0a044", + "id": "d8c84470-d8b3-4c46-a645-01cc6b139b3b", "url": "https://www.cyberbackgroundchecks.com/people/${firstName}-${lastName}/${state}/${city}" }, { "actionType": "extract", - "id": "f36a73d7-9efb-452e-8c60-6d9df2964bcf", + "id": "b4c12cf2-0fd6-4209-8816-3bf2cce23cde", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json index 3dfe8e5431..9949e04675 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/dataveria.com.json @@ -1,8 +1,9 @@ { - "name": "dataveria.com", - "version": "0.1.1", + "name": "Dataveria", + "url": "dataveria.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "fc449310-7b7b-45d4-bcf9-0c5d51c246f8", + "id": "a8f3a259-2d39-4ae3-ac13-65aa63a53331", "url": "https://dataveria.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "0481dc49-43e8-4af0-b697-680fb57ec24b", + "id": "e810cc23-2d2a-4e6e-b06f-dfc8a2e1e85d", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json index 4462e0c86d..9c1129a333 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/fastbackgroundcheck.com.json @@ -1,8 +1,9 @@ { - "name": "fastbackgroundcheck.com", - "version": "0.1.1", + "name": "FastBackgroundCheck.com", + "url": "fastbackgroundcheck.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678082400000, + "addedDatetime": 1706248800000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2a3a5979-9de0-44b2-ae03-f25422f0c2aa", + "id": "997adf8d-023c-409e-9206-57871cd25f0a", "url": "https://www.fastbackgroundcheck.com/people/${firstName}-${lastName}/${city}-${state}" }, { "actionType": "extract", - "id": "4818ff1c-d419-44c2-8168-501b456c6c6a", + "id": "2f531e34-2ac0-4743-a760-065187d6c951", "selector": ".person-container", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json index 2c215abdf6..9d114d48b3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/freepeopledirectory.com.json @@ -1,6 +1,7 @@ { - "name": "freepeopledirectory.com", - "version": "0.1.1", + "name": "FreePeopleDirectory", + "url": "freepeopledirectory.com", + "version": "0.1.4", "parent": "spokeo.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,17 +11,21 @@ "actions": [ { "actionType": "navigate", - "id": "815a1cd3-2577-4f43-a163-0cf4d22e66a4", + "id": "b8b912b0-201d-4cd1-8237-235c34fe0fea", "url": "https://www.freepeopledirectory.com/name/${firstName}-${lastName}/${state|upcase}/${city}" }, { "actionType": "extract", - "id": "10738ba0-bc6b-42ba-a37c-487ff3927dd5", + "id": "50e30922-ef1d-4820-abbd-f536378472d4", "selector": ".whole-card", "profile": { "name": { "selector": ".card-title" }, + "alternativeNamesList": { + "selector": ".//h3/span[contains(text(),'AKA:')]/following-sibling::span", + "afterText": "No other aliases." + }, "addressCityState": { "selector": ".city" }, @@ -49,4 +54,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} \ No newline at end of file +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json index 961bb83ae3..2c035a980c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/inforver.com.json @@ -1,8 +1,9 @@ { - "name": "inforver.com", - "version": "0.1.1", + "name": "Inforver", + "url": "inforver.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "a56ab792-fc1b-4e60-b0b9-0bd4f580476f", + "id": "85fac850-36ad-4d9c-ad7c-c1250c7b5585", "url": "https://inforver.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "591ba784-106c-421b-b188-a376f1f9cb01", + "id": "e5e9c1b0-4af4-4fb6-bd2d-7d026ffd95e7", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json index 31b5dc20a8..5f7e750909 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/kwold.com.json @@ -1,6 +1,7 @@ { - "name": "kwold.com", - "version": "0.1.1", + "name": "Kwold", + "url": "kwold.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "47152fc1-79d5-4bcc-b930-6b5cdc66e972", + "id": "936eee30-d31e-48fb-8cc4-9391869934b9", "url": "https://kwold.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "50507ab4-2e75-4f1d-af23-9725b9955bc3", + "id": "870ee174-275a-4ea8-b2d7-a222418e5de9", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json index d640212852..92a0d2af57 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json @@ -1,7 +1,8 @@ { - "name": "neighbor.report", + "name": "Neighbor Report", + "url": "neighbor.report", "version": "0.1.4", - "addedDatetime": 1703559600000, + "addedDatetime": 1703570400000, "steps": [ { "stepType": "scan", @@ -9,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "a554d7d2-f348-487a-97de-8d4f0d1d35c0", + "id": "bbaf8a18-fef8-42a6-9682-747b8ff485b2", "url": "https://neighbor.report/${firstName}-${lastName}/${state|stateFull|hyphenated}/${city|hyphenated}" }, { "actionType": "extract", - "id": "17f80250-1e3c-4e55-8e50-68fe98a6ce23", + "id": "0dac4a6d-1291-47c3-97b8-56200f751ac8", "selector": ".lstd", "profile": { "name": { @@ -50,12 +51,12 @@ "actions": [ { "actionType": "navigate", - "id": "59cc488d-e317-4fb4-8aaa-a20cb71f7480", + "id": "b1f7f4ab-51b0-4885-ba73-97be0822d0ba", "url": "https://neighbor.report/remove" }, { "actionType": "fillForm", - "id": "3a4c1775-941a-4f48-873d-c780f5ea25a0", + "id": "743afa6c-7dea-4115-934b-bea369307acd", "selector": ".form-horizontal", "elements": [ { @@ -74,17 +75,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "f470e245-d5ee-4908-bd6c-16e604a1a29b", + "id": "24ce0da0-7cc3-47e7-bf8e-6f5fe98b7a91", "selector": ".recaptcha-div" }, { "actionType": "solveCaptcha", - "id": "7f0a8fc6-32a3-4f4a-b61d-267f9666de91", + "id": "b720de9a-f519-466f-980d-d9c52d8870a2", "selector": ".recaptcha-div" }, { "actionType": "click", - "id": "7fbc5a97-bc57-41bc-a556-0fdfd8a0845d", + "id": "46690938-f112-4091-bd07-b5641e38151f", "elements": [ { "type": "button", @@ -94,7 +95,7 @@ }, { "actionType": "click", - "id": "d1513f65-a746-4597-9ed2-4cd5e40dead3", + "id": "07cfed17-9d75-471a-b6a0-0522add35ffa", "elements": [ { "type": "button", @@ -108,7 +109,7 @@ }, { "actionType": "expectation", - "id": "8acd9c96-443d-4593-a3a7-9efc9fd5070a", + "id": "ebd61347-60e1-4c19-bc41-dd1ce36d3138", "expectations": [ { "type": "text", @@ -125,4 +126,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json index ddb83134f9..54f8d23ac8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/newenglandfacts.com.json @@ -1,6 +1,7 @@ { - "name": "newenglandfacts.com", - "version": "0.1.1", + "name": "New England Facts", + "url": "newenglandfacts.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "8bd7953c-ee22-49be-8937-a1798046a0c1", + "id": "05725a5a-ec3f-49c8-875b-ab9787b9385f", "url": "https://newenglandfacts.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "4012d312-2f7f-4cc1-bf7a-b7655f550c1a", + "id": "7f41b78a-bb65-4bb2-a6ca-1a6ab55890ce", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json index a7b4efd714..9cb63483be 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/officialusa.com.json @@ -1,8 +1,9 @@ { - "name": "officialusa.com", - "version": "0.1.0", + "name": "OfficialUSA", + "url": "officialusa.com", + "version": "0.1.4", "parent": "neighbor.report", - "addedDatetime": 1692590400000, + "addedDatetime": 1692594000000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "dad25b4c-743b-4bca-a395-05f1e76ef5c9", + "id": "b430e29e-89f0-4994-96b2-08d0cbdc388c", "url": "https://officialusa.com/names/${firstName}-${lastName}/" }, { "actionType": "extract", - "id": "b867d570-6124-40d9-9076-7ee0fa5b4d68", + "id": "d989f3b7-9b8a-44a6-a51e-70762255f3fc", "selector": ".person", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json index 7b1d26eb38..b8550c93e4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/people-background-check.com.json @@ -1,6 +1,7 @@ { - "name": "people-background-check.com", - "version": "0.1.1", + "name": "People Background Check", + "url": "people-background-check.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1702965600000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "18e35c3b-b837-40e9-b353-20230d36bc4d", + "id": "6fee90c5-5f7e-4fd0-badf-069e2b94a65d", "url": "https://people-background-check.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}" }, { "actionType": "extract", - "id": "7a23b927-acfc-4d29-b4b6-3f204687619c", + "id": "ee03ba42-e9a5-4489-a7d6-d50bf21238aa", "selector": ".b-pfl-list", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json index 7e690167c8..34bc5b8770 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplefinders.com.json @@ -1,7 +1,8 @@ { - "name": "peoplefinders.com", - "version": "0.1.0", - "addedDatetime": 1677128400000, + "name": "PeopleFinders", + "url": "peoplefinders.com", + "version": "0.1.4", + "addedDatetime": 1677132000000, "steps": [ { "stepType": "scan", @@ -9,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "aafba5bd-a157-4e35-b653-0797a732d94c", + "id": "71c7cb2f-14fe-43b8-9623-452b8bd10d4e", "url": "https://www.peoplefinders.com/people/${firstName}-${lastName}/${state}/${city}?landing=all&age=${age}" }, { "actionType": "extract", - "id": "b8f10f20-3363-4781-a03b-c4958b6269c7", + "id": "5c5af912-091f-4f48-922f-ba554951ddd9", "selector": ".record", "profile": { "name": { @@ -48,12 +49,12 @@ "actions": [ { "actionType": "navigate", - "id": "f5fbd4f5-23f7-45ed-a9ce-3e9b0a5a7a0a", + "id": "4b065fde-35c7-43d7-aed6-3abcdac94f08", "url": "https://www.peoplefinders.com/opt-out" }, { "actionType": "click", - "id": "7b33cd1b-3948-4454-8434-e703cc235123", + "id": "b5c0929e-e362-4570-815b-0433ef97fddf", "elements": [ { "type": "button", @@ -63,7 +64,7 @@ }, { "actionType": "fillForm", - "id": "32056b7a-dc80-4d5d-b9cc-dccd32cb56be", + "id": "2fb91804-e5ea-414e-9354-fba98f3c00e1", "selector": ".opt-out-form", "elements": [ { @@ -78,17 +79,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "5d5068aa-5c16-4fdc-8f3b-e412ad4eabed", + "id": "4b9706ef-dd9b-47d6-b337-12f66a5f9138", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "3443e060-8aee-4bd0-ab2c-ea03f8b8f93c", + "id": "770019d3-fa88-400a-8480-7cc31d6b3382", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "cb9ef5b0-0155-42f1-a766-145b3c14586b", + "id": "a7285f44-6c99-44b1-8199-eb6c383fe12b", "elements": [ { "type": "button", @@ -98,22 +99,22 @@ }, { "actionType": "emailConfirmation", - "id": "5cc7cfa5-e8ab-4dc1-b58b-973af3d3f364", + "id": "05cc08ea-fb80-40fb-8cce-3ca674eea03b", "pollingTime": 30 }, { "actionType": "getCaptchaInfo", - "id": "3a44c15d-1dd0-4e92-beba-bf3d8544c6e9", + "id": "8cb4256a-b162-407f-8434-5536c7560c98", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "d2566371-8b02-4414-9a24-1f9d2761eb1d", + "id": "38c64eec-6bd9-4751-a7cf-8cbe9901b0f6", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "259d8895-ac58-46b0-a209-7f209171e13c", + "id": "d1d25423-912b-4828-825b-eb83809ada08", "elements": [ { "type": "button", @@ -123,7 +124,7 @@ }, { "actionType": "expectation", - "id": "ed02f55b-67b3-4efc-a3cc-ce6b6c7ceeed", + "id": "fdb755da-8970-426f-b09e-12165c2169dd", "expectations": [ { "type": "url", @@ -139,4 +140,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json index 6e477c6e13..6189f3d311 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/peoplesearchnow.com.json @@ -1,6 +1,7 @@ { - "name": "peoplesearchnow.com", - "version": "0.1.1", + "name": "People Search Now", + "url": "peoplesearchnow.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1705989600000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "b6994b26-9904-407b-9bcf-0fd6f809771d", + "id": "db9e093d-68c2-45e1-a529-29a2dc67dfab", "url": "https://peoplesearchnow.com/person/${firstName}-${lastName}_${city}_${state}/" }, { "actionType": "extract", - "id": "4e7f0e9a-1d24-47c0-886f-a08d88074878", + "id": "78912133-761b-4971-9780-4e16c8dd43b2", "selector": ".result-search-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json index 503392f378..815fdcb8fc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/pub360.com.json @@ -1,8 +1,9 @@ { - "name": "pub360.com", - "version": "0.1.1", + "name": "Pub360", + "url": "pub360.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "72fc91c4-e8bc-4656-8260-cd3bb15e2001", + "id": "8e2a1251-2685-476a-b4c1-53d138331abe", "url": "https://pub360.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "2cb4778d-e3d6-4432-8421-84438c280e19", + "id": "9ce62e6f-b103-45f6-9f92-56785eb22320", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json index 991d4d2b2c..5ea3d241e4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/publicreports.com.json @@ -1,6 +1,7 @@ { - "name": "publicreports.com", - "version": "0.1.1", + "name": "PublicReports", + "url": "publicreports.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "ae0104a2-a75c-4d97-bada-dda4f21dd446", + "id": "b995b1bf-6610-4085-9d07-d38857807535", "url": "https://publicreports.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "388c55e3-fa12-4376-9a02-01190b8a30fd", + "id": "7fb121fb-e2a0-4fa2-9b97-51130104971c", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json index 7409fd240b..e8b18f9ec8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/quickpeopletrace.com.json @@ -1,6 +1,7 @@ { - "name": "quickpeopletrace.com", - "version": "0.1.1", + "name": "Quick People Trace", + "url": "quickpeopletrace.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "45443ab2-7563-4c7d-8bf2-b1b550f4b825", + "id": "2db8c120-a8c3-4aa0-a9ce-b075ca85fc68", "url": "https://www.quickpeopletrace.com/search/?addresssearch=1&tabid=1&teaser-firstname=${firstName}&teaser-middlename=&teaser-lastname=${lastName}&teaser-city=${city}&teaser-state=${state|upcase}&teaser-submitted=Search" }, { "actionType": "extract", - "id": "08607047-96e8-4fbb-9af9-bf7b8e163b20", + "id": "bd48b737-89c4-408a-a28c-2dfa828aebd8", "selector": "//table/tbody/tr[position() > 1]", "profile": { "name": { @@ -24,9 +25,6 @@ "age": { "selector": ".//td[3]" }, - "addressCityState": { - "selector": ".//td[4]/strong" - }, "addressCityStateList": { "selector": ".//td[4]" }, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json index 535ec0f63d..4a68c912e3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/searchpeoplefree.com.json @@ -1,6 +1,7 @@ { - "name": "searchpeoplefree.com", - "version": "0.1.1", + "name": "Search People FREE", + "url": "searchpeoplefree.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2b537ff2-8967-465c-ad5c-c4c2d31f60e1", + "id": "f5bad072-6f55-4357-b23b-1df4c9584e67", "url": "https://searchpeoplefree.com/find/${firstName}-${lastName}/${state}/${city}" }, { "actionType": "extract", - "id": "70728718-fe02-43f6-b86f-6d6c6bbbf009", + "id": "749fb8fe-9994-41e2-a0ea-ae6334c5aee0", "selector": "//li[@class='toc l-i mb-5']", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json index 31424db72d..23f588c796 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/smartbackgroundchecks.com.json @@ -1,6 +1,7 @@ { - "name": "smartbackgroundchecks.com", - "version": "0.1.1", + "name": "SmartBackgroundChecks", + "url": "smartbackgroundchecks.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1678082400000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "97b307c8-e3e4-4090-a6ab-c5eeb599d248", + "id": "1c6bdc6e-12dd-47db-b5b0-13055c1f3d5d", "url": "https://www.smartbackgroundchecks.com/people/${firstName}-${lastName}/${city}/${state}" }, { "actionType": "extract", - "id": "ca20a933-b703-427e-8cbf-e2f25cd763a6", + "id": "ac554b4f-e4a0-44c5-81a6-c04e46e4ce3b", "selector": ".card-block", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json index 2618099829..3c0f0008f8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/spokeo.com.json @@ -1,12 +1,33 @@ { - "name": "spokeo.com", - "version": "0.1.3", + "name": "Spokeo", + "url": "spokeo.com", + "version": "0.1.4", "addedDatetime": 1692594000000, "mirrorSites": [ - { "name": "callersmart.com", "addedAt": 1705599286529, "removedAt": null }, - { "name": "selfie.network", "addedAt": 1705599286529, "removedAt": null }, - { "name": "selfie.systems", "addedAt": 1705599286529, "removedAt": null }, - { "name": "peoplewin.com", "addedAt": 1705599286529, "removedAt": null } + { + "name": "CallerSmart", + "url": "callersmart.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Selfie Network", + "url": "selfie.network", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Selfie Systems", + "url": "selfie.systems", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "PeopleWin", + "url": "peoplewin.com", + "addedAt": 1705599286529, + "removedAt": null + } ], "steps": [ { @@ -15,12 +36,12 @@ "actions": [ { "actionType": "navigate", - "id": "d3174bd8-3253-45e3-88f0-1366882a2df7", + "id": "9b617d27-b330-46fc-bdb0-6239c0873897", "url": "https://www.spokeo.com/${firstName}-${lastName}/${state|stateFull}/${city}" }, { "actionType": "extract", - "id": "e47f5f27-dfbf-4f2c-8d7a-43f581abdaa2", + "id": "4f7124c2-bd8c-4649-84f2-04f0962225b5", "selector": ".single-column-list-item", "profile": { "name": { @@ -52,12 +73,12 @@ "actions": [ { "actionType": "navigate", - "id": "dba8f444-a433-4ad2-9819-3c555bfedd9c", + "id": "df75e4fb-f14b-4b65-afe2-82e03b71c6a9", "url": "https://www.spokeo.com/optout" }, { "actionType": "fillForm", - "id": "b1145fca-3e35-4ee9-86f2-e35c393846d3", + "id": "42cbfc2b-d96b-4bd6-8d16-0542a672d869", "selector": ".optout_container", "elements": [ { @@ -72,17 +93,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "8f9608b4-bbf7-4540-8b22-5c381225cd02", + "id": "e1581b9e-7460-4bbd-a010-634c2db12ca1", "selector": "#g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "a5a884b8-12f6-4029-aa80-244f1a163f67", + "id": "01ca39d9-e842-41cf-b0f9-a7d517bc0dd6", "selector": "#g-recaptcha" }, { "actionType": "click", - "id": "a7d4fdd4-30b8-46f7-8700-67c633da1f91", + "id": "7556edd5-570b-4c4a-acc7-f1066138d513", "elements": [ { "type": "button", @@ -92,7 +113,7 @@ }, { "actionType": "expectation", - "id": "d4a804a3-de62-4f66-a56e-e9d7e65fb8bb", + "id": "f7b5125e-0dda-4a14-8943-8c20c09125bc", "expectations": [ { "type": "text", @@ -103,12 +124,12 @@ }, { "actionType": "emailConfirmation", - "id": "5138062c-99d3-4523-b222-8123b13bc524", + "id": "dbd875b6-bdc7-48ca-962b-885941e6284a", "pollingTime": 30 }, { "actionType": "expectation", - "id": "cc14f3ea-35f8-4d31-a280-dc97526de12a", + "id": "b2f1c371-d779-4b3b-8516-0d13169cf873", "expectations": [ { "type": "text", @@ -125,4 +146,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json index 84d468f943..a226c959a0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/truepeoplesearch.com.json @@ -1,6 +1,7 @@ { - "name": "truepeoplesearch.com", - "version": "0.1.1", + "name": "TruePeopleSearch", + "url": "truepeoplesearch.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1703138400000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "4b7d8751-d3cd-4e4b-b2a2-66219eb6a8e8", + "id": "12eb70c1-53d5-4881-9dce-74ed4fada583", "url": "https://www.truepeoplesearch.com/results?name=${firstName}%20${lastName}&citystatezip=${city|capitalize},${state|upcase}" }, { "actionType": "extract", - "id": "cdb5940a-8505-4b28-9699-d98235e1fff1", + "id": "881e0e21-c375-4083-a9be-86f82063849b", "selector": ".card-summary", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json index 71c8e711ed..b8fba84277 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usa-people-search.com.json @@ -1,8 +1,9 @@ { - "name": "usa-people-search.com", - "version": "0.1.1", + "name": "USA People Search", + "url": "usa-people-search.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "2c4b31a3-661b-4f30-a4d3-b5a4a13c95db", + "id": "67e80e69-f542-4714-8705-c43af630ac72", "url": "https://usa-people-search.com/name/${firstName|downcase}-${lastName|downcase}/${city|downcase}-${state|stateFull|downcase}?age=${age}" }, { "actionType": "extract", - "id": "20a4d510-56b6-46a8-92ce-be16ed3ce049", + "id": "c0a82b15-7564-4e12-8c4e-084174242623", "selector": ".card-block", "profile": { "name": { @@ -62,4 +63,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json index 412a14d7d9..4645c85dd0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usatrace.com.json @@ -1,6 +1,7 @@ { - "name": "usatrace.com", - "version": "0.1.1", + "name": "USA Trace", + "url": "usatrace.com", + "version": "0.1.4", "parent": "peoplefinders.com", "addedDatetime": 1674540000000, "steps": [ @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "5071e480-88ae-49b5-91b6-1daf26c55acf", + "id": "17217b04-28ae-4262-aa33-ee3695bb6bd6", "url": "https://www.usatrace.com/people-search/${firstName}-${lastName}/${city}-${state|upcase}" }, { "actionType": "extract", - "id": "3237fc09-247c-4942-9920-9bbb937f6ac2", + "id": "426d8e8a-2f32-46f3-9d1d-e7f6e2fddadb", "selector": "//table/tbody/tr[position() > 1]", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json index 0770b2f474..5aff073d4a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/usphonebook.com.json @@ -1,8 +1,9 @@ { - "name": "usphonebook.com", - "version": "0.1.1", + "name": "USPhoneBook", + "url": "usphonebook.com", + "version": "0.1.4", "parent": "peoplefinders.com", - "addedDatetime": 1678078800000, + "addedDatetime": 1678082400000, "steps": [ { "stepType": "scan", @@ -10,12 +11,12 @@ "actions": [ { "actionType": "navigate", - "id": "f214150b-4f02-46e1-b7ea-81f6bb1bf097", + "id": "6ee93554-95da-4a36-a7f7-c059d8f53ca3", "url": "https://www.usphonebook.com/${firstName}-${lastName}/${state|stateFull}/${city}" }, { "actionType": "extract", - "id": "af98bb63-b885-4f47-bb47-5f9ec5b491a4", + "id": "fffae12f-4ca1-4a8f-81b9-00adf0487129", "selector": ".ls_contacts-people-finder-wrapper", "profile": { "name": { @@ -56,4 +57,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json index f493fbd347..5aff5bd46e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/verecor.com.json @@ -1,7 +1,8 @@ { - "name": "verecor.com", - "version": "0.1.2", - "addedDatetime": 1677128400000, + "name": "Verecor", + "url": "verecor.com", + "version": "0.1.4", + "addedDatetime": 1677132000000, "steps": [ { "stepType": "scan", @@ -9,7 +10,7 @@ "actions": [ { "actionType": "navigate", - "id": "6f53d146-af6a-4bce-970d-f1dcbc496037", + "id": "37fc63a6-e434-4ba0-9e9e-d80898e4dfa4", "url": "https://verecor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -23,7 +24,7 @@ }, { "actionType": "extract", - "id": "e8c09200-030c-492a-8e54-22bc6bdb6829", + "id": "a955924c-7959-48c8-9511-3f843baed729", "selector": ".search-item", "profile": { "name": { @@ -58,12 +59,12 @@ "actions": [ { "actionType": "navigate", - "id": "f4bbe480-a6ff-40a5-aa25-8ff9ac40c9bf", + "id": "85cd9682-94d8-46ac-9999-e03dfa9f8d4e", "url": "https://verecor.com/ng/control/privacy" }, { "actionType": "fillForm", - "id": "1e6302e1-daf9-49d6-951f-506ea5e266a0", + "id": "ed45c76b-e537-4072-9f46-9515c6e215be", "selector": ".ahm", "elements": [ { @@ -82,17 +83,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "6a3dc470-3bf7-4b8b-bb44-f77ef1a2c540", + "id": "0e1474f0-24fe-4f6a-8d2e-2dfd91cf574b", "selector": ".g-recaptcha" }, { "actionType": "solveCaptcha", - "id": "83157244-c5bf-44a9-979c-679e1404d67d", + "id": "52a858f5-7dc5-40aa-aaa7-7090e06ea55e", "selector": ".g-recaptcha" }, { "actionType": "click", - "id": "15c50d7f-0e72-4509-be2b-40cde34b48e6", + "id": "759e0dd2-3a93-42a8-9a83-5e3408f5566b", "elements": [ { "type": "button", @@ -102,7 +103,7 @@ }, { "actionType": "expectation", - "id": "2ed336a2-a7a9-4cbd-933c-cd463df4f553", + "id": "089924be-5ea3-48a9-a325-8976d262f39b", "expectations": [ { "type": "text", @@ -113,12 +114,12 @@ }, { "actionType": "emailConfirmation", - "id": "88c09081-e848-4e75-a7b9-3ee28e95a459", + "id": "8094718e-412a-418f-b74d-cd4fc5e42c56", "pollingTime": 30 }, { "actionType": "expectation", - "id": "dd03cf9f-8227-4881-86bf-09ce158bf151", + "id": "af8fb89b-88d2-4901-b90c-eaac3c7566db", "expectations": [ { "type": "text", @@ -135,4 +136,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json index 814e908fae..c7a3bad5c2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/vericora.com.json @@ -1,8 +1,9 @@ { - "name": "vericora.com", - "version": "0.1.1", + "name": "Vericora", + "url": "vericora.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "9488e141-d109-4cbf-bc65-1b9036728ff4", + "id": "69175f1a-0024-4efd-ab3e-67bcf915a770", "url": "https://vericora.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "baaecb74-8d63-496c-a3e0-a8acbdee2c99", + "id": "bd941009-4462-4d59-ba44-46250f580531", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json index 121bc68d3e..5f4f307f92 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veriforia.com.json @@ -1,8 +1,9 @@ { - "name": "veriforia.com", - "version": "0.1.1", + "name": "Veriforia", + "url": "veriforia.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1677733200000, + "addedDatetime": 1677736800000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "ffb30e97-b03f-4157-a511-09ad8ffb8b54", + "id": "17442975-944c-4b01-8518-7f1dff171ad2", "url": "https://veriforia.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "1d8d9c20-9897-4386-8bc1-bd591abe7c81", + "id": "32e963e1-4959-4e5e-981b-550f1bf36f9a", "selector": ".search-item", "profile": { "name": { @@ -64,4 +65,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json index 43d95caf8a..61becad701 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/veripages.com.json @@ -1,8 +1,9 @@ { - "name": "veripages.com", - "version": "0.1.2", + "name": "Veripages", + "url": "veripages.com", + "version": "0.1.4", "parent": "verecor.com", - "addedDatetime": 1691982000000, + "addedDatetime": 1691989200000, "steps": [ { "stepType": "scan", @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "5bf98772-1804-4939-a06b-dbf9cd31f198", + "id": "2346b569-1c46-4ef9-8ea0-fa18bea967fa", "url": "https://veripages.com/inner/profile/search?fname=${firstName}&lname=${lastName}&fage=${age|ageRange}&state=${state}&city=${city}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "f3d53642-9a58-4275-b97f-5547e3ef8e55", + "id": "c4281ca8-d4d0-4091-b6c2-3094801e99c0", "selector": ".search-item", "profile": { "name": { @@ -66,4 +67,4 @@ "confirmOptOutScan": 72, "maintenanceScan": 240 } -} +} \ No newline at end of file diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json index d12865681f..3d94019338 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/virtory.com.json @@ -1,6 +1,7 @@ { - "name": "virtory.com", - "version": "0.1.1", + "name": "Virtory", + "url": "virtory.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "99465bd6-ce87-4fc6-96a2-eea8137e4a30", + "id": "0568e4f5-73c2-4b1a-9eb6-ac3571b1a01e", "url": "https://virtory.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "8d66ac98-f788-4fbd-acec-56034682b4b1", + "id": "df2216f3-0890-4d13-b2aa-233084167720", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json index b4fd3669ec..12c43b7fa4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/wellnut.com.json @@ -1,6 +1,7 @@ { - "name": "wellnut.com", - "version": "0.1.1", + "name": "Wellnut", + "url": "wellnut.com", + "version": "0.1.4", "parent": "verecor.com", "addedDatetime": 1703052000000, "steps": [ @@ -10,7 +11,7 @@ "actions": [ { "actionType": "navigate", - "id": "b9db3c1e-ece6-45d1-94ec-1143da9607aa", + "id": "a38752f3-ae69-45c3-ba3f-3a73e549e644", "url": "https://wellnut.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${age|ageRange}", "ageRange": [ "18-30", @@ -24,7 +25,7 @@ }, { "actionType": "extract", - "id": "e4b7c983-c96e-4ce8-8703-3cb319454db7", + "id": "b7747e92-5fe5-46f7-b083-5df6fbdc2b84", "selector": ".card", "profile": { "name": { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index 0e6b930d71..0eddaad17e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -32,6 +32,7 @@ final class DataBrokerProtectionProcessor { private let operationQueue: OperationQueue private var pixelHandler: EventMapping private let userNotificationService: DataBrokerProtectionUserNotificationService + private let engagementPixels: DataBrokerProtectionEngagementPixels init(database: DataBrokerProtectionRepository, config: SchedulerConfig, @@ -48,6 +49,7 @@ final class DataBrokerProtectionProcessor { self.pixelHandler = pixelHandler self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsDifferentBrokers self.userNotificationService = userNotificationService + self.engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) } // MARK: - Public functions @@ -106,12 +108,14 @@ final class DataBrokerProtectionProcessor { completion: @escaping () -> Void) { // Before running new operations we check if there is any updates to the broker files. - // This runs only once per 24 hours. if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault) brokerUpdater.checkForUpdatesInBrokerJSONFiles() } + // This will fire the DAU/WAU/MAU pixels, + engagementPixels.fireEngagementPixel() + let brokersProfileData = database.fetchAllBrokerProfileQueryData() let dataBrokerOperationCollections = createDataBrokerOperationCollections(from: brokersProfileData, operationType: operationType, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift index 75b80b195a..038c72c2d8 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift @@ -30,7 +30,7 @@ public enum EmailError: Error, Equatable, Codable { } protocol EmailServiceProtocol { - func getEmail(dataBrokerName: String?) async throws -> String + func getEmail(dataBrokerURL: String?) async throws -> String func getConfirmationLink(from email: String, numberOfRetries: Int, pollingIntervalInSeconds: Int, @@ -51,10 +51,10 @@ struct EmailService: EmailServiceProtocol { self.redeemUseCase = redeemUseCase } - func getEmail(dataBrokerName: String? = nil) async throws -> String { + func getEmail(dataBrokerURL: String? = nil) async throws -> String { var urlString = Constants.baseUrl + "/generate" - if let dataBrokerValue = dataBrokerName { + if let dataBrokerValue = dataBrokerURL { urlString += "?dataBroker=\(dataBrokerValue)" } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift index ab59f166cc..a70ad31818 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseProvider.swift @@ -34,7 +34,7 @@ protocol DataBrokerProtectionDatabaseProvider: SecureStorageDatabaseProvider { func save(_ broker: BrokerDB) throws -> Int64 func update(_ broker: BrokerDB) throws func fetchBroker(with id: Int64) throws -> BrokerDB? - func fetchBroker(with name: String) throws -> BrokerDB? + func fetchBroker(with url: String) throws -> BrokerDB? func fetchAllBrokers() throws -> [BrokerDB] func save(_ profileQuery: ProfileQueryDB) throws -> Int64 @@ -85,6 +85,8 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba public init(file: URL = DefaultDataBrokerProtectionDatabaseProvider.defaultDatabaseURL(), key: Data) throws { try super.init(file: file, key: key, writerType: .pool) { migrator in migrator.registerMigration("v1", migrate: Self.migrateV1(database:)) + migrator.registerMigration("v2", migrate: Self.migrateV2(database:)) + } } @@ -259,6 +261,16 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba $0.column(OptOutAttemptDB.Columns.startDate.name, .date).notNull() } } + + static func migrateV2(database: Database) throws { + try database.alter(table: BrokerDB.databaseTableName) { + $0.add(column: BrokerDB.Columns.url.name, .text) + } + try database.execute(sql: """ + UPDATE \(BrokerDB.databaseTableName) SET \(BrokerDB.Columns.url.name) = \(BrokerDB.Columns.name.name) + """) + } + // swiftlint:enable function_body_length func updateProfile(profile: DataBrokerProtectionProfile, mapperToDB: MapperToDB) throws -> Int64 { @@ -359,10 +371,10 @@ final class DefaultDataBrokerProtectionDatabaseProvider: GRDBSecureStorageDataba } } - func fetchBroker(with name: String) throws -> BrokerDB? { + func fetchBroker(with url: String) throws -> BrokerDB? { try db.read { db in return try BrokerDB - .filter(Column(BrokerDB.Columns.name.name) == name) + .filter(Column(BrokerDB.Columns.url.name) == url) .fetchOne(db) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift index 267b013ed8..81cf02f70d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/Mappers.swift @@ -58,7 +58,7 @@ struct MapperToDB { func mapToDB(_ broker: DataBroker, id: Int64? = nil) throws -> BrokerDB { let encodedBroker = try jsonEncoder.encode(broker) - return .init(id: id, name: broker.name, json: encodedBroker, version: broker.version) + return .init(id: id, name: broker.name, json: encodedBroker, version: broker.version, url: broker.url) } func mapToDB(_ profileQuery: ProfileQuery, relatedTo profileId: Int64) throws -> ProfileQueryDB { @@ -171,6 +171,7 @@ struct MapperToModel { return DataBroker( id: brokerDB.id, name: decodedBroker.name, + url: decodedBroker.url, steps: decodedBroker.steps, version: decodedBroker.version, schedulingConfig: decodedBroker.schedulingConfig, @@ -232,7 +233,8 @@ struct MapperToModel { reportId: extractedProfile.reportId, age: extractedProfile.age, email: extractedProfile.email, - removedDate: extractedProfileDB.removedDate) + removedDate: extractedProfileDB.removedDate, + identifier: extractedProfile.identifier) } func mapToModel(_ scanEvent: ScanHistoryEventDB) throws -> HistoryEvent { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift index 348d99180d..a772e4d543 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/SchedulerSchema.swift @@ -94,6 +94,7 @@ struct BrokerDB: Codable { let name: String let json: Data let version: String + let url: String } extension BrokerDB: PersistableRecord, FetchableRecord { @@ -104,6 +105,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { case name case json case version + case url } init(row: Row) throws { @@ -111,6 +113,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { name = row[Columns.name] json = row[Columns.json] version = row[Columns.version] + url = row[Columns.url] } func encode(to container: inout PersistenceContainer) throws { @@ -118,6 +121,7 @@ extension BrokerDB: PersistableRecord, FetchableRecord { container[Columns.name] = name container[Columns.json] = json container[Columns.version] = version + container[Columns.url] = url } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 63b1b4a47e..9f77b8a675 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -36,6 +36,7 @@ protocol DBPUICommunicationDelegate: AnyObject { func startScanAndOptOut() -> Bool func getInitialScanState() async -> DBPUIInitialScanState func getMaintananceScanState() async -> DBPUIScanAndOptOutMaintenanceState + func getDataBrokers() async -> [DBPUIDataBroker] } enum DBPUIReceivedMethodName: String { @@ -53,6 +54,7 @@ enum DBPUIReceivedMethodName: String { case startScanAndOptOut case initialScanStatus case maintenanceScanStatus + case getDataBrokers } enum DBPUISendableMethodName: String { @@ -69,7 +71,7 @@ struct DBPUICommunicationLayer: Subfeature { weak var delegate: DBPUICommunicationDelegate? private enum Constants { - static let version = 1 + static let version = 2 } internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable) { @@ -101,6 +103,7 @@ struct DBPUICommunicationLayer: Subfeature { case .startScanAndOptOut: return startScanAndOptOut case .initialScanStatus: return initialScanStatus case .maintenanceScanStatus: return maintenanceScanStatus + case .getDataBrokers: return getDataBrokers } } @@ -264,6 +267,11 @@ struct DBPUICommunicationLayer: Subfeature { return maintenanceScanStatus } + func getDataBrokers(params: Any, origin: WKScriptMessage) async throws -> Encodable? { + let dataBrokers = await delegate?.getDataBrokers() ?? [DBPUIDataBroker]() + return DBPUIDataBrokerList(dataBrokers: dataBrokers) + } + func sendMessageToUI(method: DBPUISendableMethodName, params: DBPUISendableMessage, into webView: WKWebView) { broker?.push(method: method.rawValue, params: params, for: self, into: webView) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index accf119bb7..ce258c3565 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -26,6 +26,7 @@ final public class DataBrokerProtectionViewController: NSViewController { private let dataManager: DataBrokerProtectionDataManaging private let scheduler: DataBrokerProtectionScheduler private var webView: WKWebView? + private var loader: NSProgressIndicator! private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable private let webUIViewModel: DBPUIViewModel @@ -63,9 +64,10 @@ final public class DataBrokerProtectionViewController: NSViewController { public override func viewDidLoad() { super.viewDidLoad() + addLoadingIndicator() reloadObserver = NotificationCenter.default.addObserver(forName: DataBrokerProtectionNotifications.shouldReloadUI, - object: nil, - queue: .main) { [weak self] _ in + object: nil, + queue: .main) { [weak self] _ in self?.webView?.reload() } } @@ -75,16 +77,39 @@ final public class DataBrokerProtectionViewController: NSViewController { webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 1024, height: 768), configuration: configuration) webView?.uiDelegate = self + webView?.navigationDelegate = self view = webView! if let url = URL(string: webUISettings.selectedURL) { webView?.load(url) } else { + removeLoadingIndicator() assertionFailure("Selected URL is not valid \(webUISettings.selectedURL)") } } + private func addLoadingIndicator() { + loader = NSProgressIndicator() + loader.wantsLayer = true + loader.style = .spinning + loader.controlSize = .regular + loader.sizeToFit() + loader.translatesAutoresizingMaskIntoConstraints = false + loader.controlSize = .large + view.addSubview(loader) + + NSLayoutConstraint.activate([ + loader.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loader.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + + private func removeLoadingIndicator() { + loader.stopAnimation(nil) + loader.removeFromSuperview() + } + deinit { if let reloadObserver { NotificationCenter.default.removeObserver(reloadObserver) @@ -98,3 +123,14 @@ extension DataBrokerProtectionViewController: WKUIDelegate { return nil } } + +extension DataBrokerProtectionViewController: WKNavigationDelegate { + + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + loader.startAnimation(nil) + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + removeLoadingIndicator() + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 836bd52a30..454f1c75a9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -26,22 +26,24 @@ struct MapperToUI { name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map(mapToUI) ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String]() + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970 ) } - func mapToUI(_ dataBrokerName: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { + func mapToUI(_ dataBrokerName: String, databrokerURL: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { DBPUIDataBrokerProfileMatch( - dataBroker: DBPUIDataBroker(name: dataBrokerName), + dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map(mapToUI) ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String]() + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970 ) } func mapToUI(_ dataBroker: DataBroker) -> DBPUIDataBroker { - DBPUIDataBroker(name: dataBroker.name) + DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url) } func mapToUI(_ address: AddressCityState) -> DBPUIUserProfileAddress { @@ -75,7 +77,7 @@ struct MapperToUI { if !$0.dataBroker.mirrorSites.isEmpty { let mirrorSitesMatches = $0.dataBroker.mirrorSites.compactMap { mirrorSite in if mirrorSite.shouldWeIncludeMirrorSite() { - return mapToUI(mirrorSite.name, extractedProfile: extractedProfile) + return mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) } return nil @@ -110,7 +112,7 @@ struct MapperToUI { if let closestMatchesFoundEvent = scanOperation.closestMatchesFoundEvent() { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { - let mirrorSiteMatch = mapToUI(mirrorSite.name, extractedProfile: extractedProfile) + let mirrorSiteMatch = mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) if let extractedProfileRemovedDate = extractedProfile.removedDate, mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { @@ -124,11 +126,21 @@ struct MapperToUI { } let completedOptOutsDictionary = Dictionary(grouping: removedProfiles, by: { $0.dataBroker }) - let completedOptOuts = completedOptOutsDictionary.map { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in - DBPUIOptOutMatch(dataBroker: key, matches: value.count) - } - let lastScans = getLastScanInformation(brokerProfileQueryData: brokerProfileQueryData) - let nextScans = getNextScansInformation(brokerProfileQueryData: brokerProfileQueryData) + let completedOptOuts: [DBPUIOptOutMatch] = completedOptOutsDictionary.compactMap { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in + value.compactMap { match in + guard let removedDate = match.date else { return nil } + return DBPUIOptOutMatch(dataBroker: key, + matches: value.count, + name: match.name, + alternativeNames: match.alternativeNames, + addresses: match.addresses, + date: removedDate) + } + }.flatMap { $0 } + + let nearestScanByBrokerURL = nearestRunDates(for: brokerProfileQueryData) + let lastScans = getLastScanInformation(brokerProfileQueryData: brokerProfileQueryData, nearestScanOperationByBroker: nearestScanByBrokerURL) + let nextScans = getNextScansInformation(brokerProfileQueryData: brokerProfileQueryData, nearestScanOperationByBroker: nearestScanByBrokerURL) return DBPUIScanAndOptOutMaintenanceState( inProgressOptOuts: inProgressOptOuts, @@ -140,7 +152,8 @@ struct MapperToUI { private func getLastScanInformation(brokerProfileQueryData: [BrokerProfileQueryData], currentDate: Date = Date(), - format: String = "dd/MM/yyyy") -> DBUIScanDate { + format: String = "dd/MM/yyyy", + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { let scansGroupedByLastRunDate = Dictionary(grouping: brokerProfileQueryData, by: { $0.scanOperationData.lastRunDate?.toFormat(format) }) let closestScansBeforeToday = scansGroupedByLastRunDate .filter { $0.key != nil && $0.key!.toDate(using: format) < currentDate } @@ -148,12 +161,13 @@ struct MapperToUI { .flatMap { [$0.key?.toDate(using: format): $0.value] } .last - return scanDate(element: closestScansBeforeToday) + return scanDate(element: closestScansBeforeToday, nearestScanOperationByBroker: nearestScanOperationByBroker) } private func getNextScansInformation(brokerProfileQueryData: [BrokerProfileQueryData], currentDate: Date = Date(), - format: String = "dd/MM/yyyy") -> DBUIScanDate { + format: String = "dd/MM/yyyy", + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { let scansGroupedByPreferredRunDate = Dictionary(grouping: brokerProfileQueryData, by: { $0.scanOperationData.preferredRunDate?.toFormat(format) }) let closestScansAfterToday = scansGroupedByPreferredRunDate .filter { $0.key != nil && $0.key!.toDate(using: format) > currentDate } @@ -161,22 +175,50 @@ struct MapperToUI { .flatMap { [$0.key?.toDate(using: format): $0.value] } .first - return scanDate(element: closestScansAfterToday) + return scanDate(element: closestScansAfterToday, nearestScanOperationByBroker: nearestScanOperationByBroker) + } + + // A dictionary containing the closest scan by broker + private func nearestRunDates(for brokerData: [BrokerProfileQueryData]) -> [String: Date] { + let today = Date() + let nearestDates = brokerData.reduce(into: [String: Date]()) { result, data in + let url = data.dataBroker.url + if let operationDate = data.scanOperationData.preferredRunDate { + if operationDate > today { + if let existingDate = result[url] { + if operationDate < existingDate { + result[url] = operationDate + } + } else { + result[url] = operationDate + } + } + } + } + return nearestDates } - private func scanDate(element: Dictionary.Element?) -> DBUIScanDate { + private func scanDate(element: Dictionary.Element?, + nearestScanOperationByBroker: [String: Date]) -> DBUIScanDate { if let element = element, let date = element.key { return DBUIScanDate( date: date.timeIntervalSince1970, dataBrokers: element.value.flatMap { - var brokers = [DBPUIDataBroker(name: $0.dataBroker.name)] + let brokerOperationDate = nearestScanOperationByBroker[$0.dataBroker.url] + var brokers = [DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: brokerOperationDate?.timeIntervalSince1970 ?? nil)] for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: date) { - brokers.append(DBPUIDataBroker(name: mirrorSite.name)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: brokerOperationDate?.timeIntervalSince1970 ?? nil)) } return brokers } + .reduce(into: []) { result, dataBroker in // Remove dupes + guard !result.contains(where: { $0.url == dataBroker.url }) else { + return + } + result.append(dataBroker) + } ) } else { return DBUIScanDate(date: 0, dataBrokers: [DBPUIDataBroker]()) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift index 2a6108f826..a604a0a4d7 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/BrokerJSONCodableTests.swift @@ -20,11 +20,174 @@ import XCTest @testable import DataBrokerProtection final class BrokerJSONCodableTests: XCTestCase { - let verecorJSONString = """ + let verecorWithURLJSONString = """ + { + "name": "Verecor", + "url": "verecor.com", + "version": "0.1.0", + "addedDatetime": 1677128400000, + "mirrorSites": [ + { + "name": "Potato", + "url": "potato.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "Tomato", + "url": "tomato.com", + "addedAt": 1705599286529, + "removedAt": null + } + ], + "steps": [ + { + "stepType": "scan", + "scanType": "templatedUrl", + "actions": [ + { + "actionType": "navigate", + "id": "84aa05bc-1ca0-4f16-ae74-dfb352ce0eee", + "url": "https://verecor.com/profile/search?fname=${firstName}&lname=${lastName}&state=${state}&city=${city}&fage=${ageRange}", + "ageRange": [ + "18-30", + "31-40", + "41-50", + "51-60", + "61-70", + "71-80", + "81+" + ] + }, + { + "actionType": "extract", + "id": "92252eb5-ccaf-4b00-a3fe-019110ce0534", + "selector": ".search-item", + "profile": { + "name": { + "selector": "h4" + }, + "alternativeNamesList": { + "selector": ".//div[@class='col-sm-24 col-md-16 name']//li", + "findElements": true + }, + "age": { + "selector": ".age" + }, + "addressCityStateList": { + "selector": ".//div[@class='col-sm-24 col-md-8 location']//li", + "findElements": true + }, + "profileUrl": { + "selector": "a" + } + } + } + ] + }, + { + "stepType": "optOut", + "optOutType": "formOptOut", + "actions": [ + { + "actionType": "navigate", + "id": "49f9aa73-4f97-47c0-b8bf-1729e9c169c0", + "url": "https://verecor.com/ng/control/privacy" + }, + { + "actionType": "fillForm", + "id": "55b1d0bb-d303-4b6f-bf9e-3fd96746f27e", + "selector": ".ahm", + "elements": [ + { + "type": "fullName", + "selector": "#user_name" + }, + { + "type": "email", + "selector": "#user_email" + }, + { + "type": "profileUrl", + "selector": "#url" + } + ] + }, + { + "actionType": "getCaptchaInfo", + "id": "9efb1153-8f52-41e4-a8fb-3077a97a586d", + "selector": ".g-recaptcha" + }, + { + "actionType": "solveCaptcha", + "id": "ed49e4c3-0cfa-4f1e-b3d1-06ad7b8b9ba4", + "selector": ".g-recaptcha" + }, + { + "actionType": "click", + "id": "6b986aa4-3d1b-44d5-8b2b-5463ee8916c9", + "elements": [ + { + "type": "button", + "selector": ".btn-sbmt" + } + ] + }, + { + "actionType": "expectation", + "id": "d4c64d9b-1004-487e-ab06-ae74869bc9a7", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Your removal request has been received" + } + ] + }, + { + "actionType": "emailConfirmation", + "id": "3b4c611a-61ab-4792-810e-d5b3633ea203", + "pollingTime": 30 + }, + { + "actionType": "expectation", + "id": "afe805a0-d422-473c-b47f-995a8672d476", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Your information control request has been confirmed." + } + ] + } + ] + } + ], + "schedulingConfig": { + "retryError": 48, + "confirmOptOutScan": 72, + "maintenanceScan": 240 + } + } + + """ + let verecorNoURLJSONString = """ { "name": "verecor.com", "version": "0.1.0", "addedDatetime": 1677128400000, + "mirrorSites": [ + { + "name": "tomato.com", + "addedAt": 1705599286529, + "removedAt": null + }, + { + "name": "potato.com", + "addedAt": 1705599286529, + "removedAt": null + } + ], "steps": [ { "stepType": "scan", @@ -157,9 +320,27 @@ final class BrokerJSONCodableTests: XCTestCase { """ - func testVerecorJSON_isCorrectlyParsed() { + func testVerecorJSONNoURL_isCorrectlyParsed() { do { - _ = try JSONDecoder().decode(DataBroker.self, from: verecorJSONString.data(using: .utf8)!) + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorNoURLJSONString.data(using: .utf8)!) + XCTAssertEqual(broker.url, broker.name) + for mirror in broker.mirrorSites { + XCTAssertEqual(mirror.url, mirror.name) + } + } catch { + XCTFail("JSON string should be parsed correctly.") + } + } + + func testVerecorJSONWithURL_isCorrectlyParsed() { + do { + let broker = try JSONDecoder().decode(DataBroker.self, from: verecorWithURLJSONString.data(using: .utf8)!) + XCTAssertEqual(broker.url, "verecor.com") + XCTAssertEqual(broker.name, "Verecor") + + for mirror in broker.mirrorSites { + XCTAssertNotEqual(mirror.url, mirror.name) + } } catch { XCTFail("JSON string should be parsed correctly.") } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index 3a65f7ae9a..ca2ea7a3c0 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -44,7 +44,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -92,14 +92,14 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] let mockScanOperation = ScanOperationData(brokerId: brokerId, profileQueryId: profileQueryId, preferredRunDate: currentPreferredRunDate, historyEvents: historyEvents) - let extractedProfileSaved1 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc") - let extractedProfileSaved2 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "zxz") + let extractedProfileSaved1 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "abc", identifier: "abc") + let extractedProfileSaved2 = ExtractedProfile(id: 1, name: "Some name", profileUrl: "zxz", identifier: "zxz") let optOutData = [OptOutOperationData.mock(with: extractedProfileSaved1), OptOutOperationData.mock(with: extractedProfileSaved2)] @@ -143,7 +143,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -745,7 +745,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let extractedProfileId: Int64 = 1 let currentPreferredRunDate = Date() - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -770,7 +770,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase { let currentPreferredRunDate = Date() let expectedPreferredRunDate = Date().addingTimeInterval(config.confirmOptOutScan.hoursToSeconds) - let mockDataBroker = DataBroker(name: "databroker", steps: [Step](), version: "1.0", schedulingConfig: config) + let mockDataBroker = DataBroker(name: "databroker", url: "databroker.com", steps: [Step](), version: "1.0", schedulingConfig: config) let mockProfileQuery = ProfileQuery(id: profileQueryId, firstName: "a", lastName: "b", city: "c", state: "d", birthYear: 1222) let historyEvents = [HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)] @@ -846,6 +846,7 @@ extension DataBroker { DataBroker( id: 1, name: "Test broker", + url: "testbroker.com", steps: [ Step(type: .scan, actions: [Action]()), Step(type: .optOut, actions: [Action]()) @@ -863,6 +864,7 @@ extension DataBroker { DataBroker( id: 1, name: "Test broker", + url: "testbroker.com", steps: [ Step(type: .scan, actions: [Action]()), Step(type: .optOut, actions: [Action](), optOutType: .parentSiteOptOut) @@ -879,6 +881,7 @@ extension DataBroker { static var mockWithoutId: DataBroker { DataBroker( name: "Test broker", + url: "testbroker.com", steps: [Step](), version: "1.0", schedulingConfig: DataBrokerScheduleConfig( @@ -904,19 +907,19 @@ extension ProfileQuery { extension ExtractedProfile { static var mockWithRemovedDate: ExtractedProfile { - ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: Date()) + ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: Date(), identifier: "someURL") } static var mockWithoutRemovedDate: ExtractedProfile { - ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL") + ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", identifier: "someURL") } static var mockWithoutId: ExtractedProfile { - ExtractedProfile(name: "Some name", profileUrl: "someOtherURL") + ExtractedProfile(name: "Some name", profileUrl: "someOtherURL", identifier: "someOtherURL") } static func mockWithRemoveDate(_ date: Date) -> ExtractedProfile { - ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: date) + ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: date, identifier: "someURL") } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEngagementPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEngagementPixelsTests.swift new file mode 100644 index 0000000000..7779cc0d00 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionEngagementPixelsTests.swift @@ -0,0 +1,220 @@ +// +// DataBrokerProtectionEngagementPixelsTests.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 XCTest +import Foundation +@testable import DataBrokerProtection + +final class DataBrokerProtectionEngagementPixelsTests: XCTestCase { + + private let database = MockDatabase() + private let repository = MockDataBrokerProtectionEngagementPixelsRepository() + private let handler = MockDataBrokerProtectionPixelsHandler() + + private var fakeProfile: DataBrokerProtectionProfile { + let name = DataBrokerProtectionProfile.Name(firstName: "John", lastName: "Doe") + let address = DataBrokerProtectionProfile.Address(city: "City", state: "State") + + return DataBrokerProtectionProfile(names: [name], addresses: [address], phones: [String](), birthYear: 1900) + } + + override func tearDown() { + database.clear() + repository.clear() + handler.clear() + } + + func testWhenThereIsNoProfile_thenNoEngagementPixelIsFired() { + database.setFetchedProfile(nil) + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + // We test we have no interactions with the repository + XCTAssertFalse(repository.wasDailyPixelSent) + XCTAssertFalse(repository.wasWeeklyPixelSent) + XCTAssertFalse(repository.wasMonthlyPixelSent) + XCTAssertFalse(repository.wasGetLatestDailyPixelCalled) + XCTAssertFalse(repository.wasWeeklyPixelSent) + XCTAssertFalse(repository.wasMonthlyPixelSent) + + // The pixel should not be fired + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.isEmpty) + } + + func testWhenLatestDailyPixelIsNil_thenWeFireDailyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestDailyPixel = nil + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + XCTAssertTrue(wasPixelFired(.dailyActiveUser)) + XCTAssertTrue(repository.wasDailyPixelSent) + } + + func testWhenCurrentDayIsDifferentToLatestDailyPixel_thenWeFireDailyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestWeeklyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-02-21")) + + XCTAssertTrue(wasPixelFired(.dailyActiveUser)) + XCTAssertTrue(repository.wasDailyPixelSent) + } + + func testWhenCurrentDayIsEqualToLatestDailyPixel_thenWeDoNotFireDailyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestDailyPixel = Date() + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + XCTAssertFalse(wasPixelFired(.dailyActiveUser)) + XCTAssertFalse(repository.wasDailyPixelSent) + } + + func testWhenLatestWeeklyPixelIsNil_thenWeFireWeeklyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestWeeklyPixel = nil + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + XCTAssertTrue(wasPixelFired(.weeklyActiveUser)) + XCTAssertTrue(repository.wasWeeklyPixelSent) + } + + func testWhenCurrentDayIsSevenDatesEqualOrGreaterThanLatestWeekly_thenWeFireWeeklyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestWeeklyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-02-27")) + + XCTAssertTrue(wasPixelFired(.weeklyActiveUser)) + XCTAssertTrue(repository.wasWeeklyPixelSent) + } + + func testWhenCurrentDayIsSevenDatesLessThanLatestWeekly_thenWeDoNotFireWeeklyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestWeeklyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-02-26")) + + XCTAssertFalse(wasPixelFired(.weeklyActiveUser)) + XCTAssertFalse(repository.wasWeeklyPixelSent) + } + + func testWhenLatestMonthlyPixelIsNil_thenWeFireMonthlyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestMonthlyPixel = nil + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: Date()) + + XCTAssertTrue(wasPixelFired(.monthlyActiveUser)) + XCTAssertTrue(repository.wasMonthlyPixelSent) + } + + func testWhenCurrentMonthIs28DatesGreaterOrEqualThanLatestMonthlyPixel_thenWeFireMonthlyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestMonthlyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-03-19")) + + XCTAssertTrue(wasPixelFired(.monthlyActiveUser)) + XCTAssertTrue(repository.wasMonthlyPixelSent) + } + + func testWhenCurrentIsNot28DatesGreaterOrEqualToLatestMonthlyPixel_thenWeDoNotFireMonthlyPixel() { + database.setFetchedProfile(fakeProfile) + repository.setLatestMonthlyPixel = dateFromString("2024-02-20") + let sut = DataBrokerProtectionEngagementPixels(database: database, handler: handler, repository: repository) + + sut.fireEngagementPixel(currentDate: dateFromString("2024-03-18")) + + XCTAssertFalse(wasPixelFired(.monthlyActiveUser)) + XCTAssertFalse(repository.wasMonthlyPixelSent) + } + + private func wasPixelFired(_ pixel: DataBrokerProtectionPixels) -> Bool { + MockDataBrokerProtectionPixelsHandler.lastPixelsFired.contains(where: { $0.name == pixel.name }) + } + + private func dateFromString(_ string: String) -> Date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + return dateFormatter.date(from: string)! + } +} + +final class MockDataBrokerProtectionEngagementPixelsRepository: DataBrokerProtectionEngagementPixelsRepository { + var wasDailyPixelSent = false + var wasWeeklyPixelSent = false + var wasMonthlyPixelSent = false + var wasGetLatestDailyPixelCalled = false + var wasGetLatestWeeklyPixelCalled = false + var wasGetLatestMonthlyPixelCalled = false + var setLatestDailyPixel: Date? + var setLatestWeeklyPixel: Date? + var setLatestMonthlyPixel: Date? + + func markDailyPixelSent() { + wasDailyPixelSent = true + } + + func markWeeklyPixelSent() { + wasWeeklyPixelSent = true + } + + func markMonthlyPixelSent() { + wasMonthlyPixelSent = true + } + + func getLatestDailyPixel() -> Date? { + wasGetLatestDailyPixelCalled = true + return setLatestDailyPixel + } + + func getLatestWeeklyPixel() -> Date? { + wasGetLatestWeeklyPixelCalled = true + return setLatestWeeklyPixel + } + + func getLatestMonthlyPixel() -> Date? { + wasGetLatestMonthlyPixelCalled = true + return setLatestMonthlyPixel + } + + func clear() { + wasDailyPixelSent = false + wasWeeklyPixelSent = false + wasMonthlyPixelSent = false + wasGetLatestDailyPixelCalled = false + wasGetLatestWeeklyPixelCalled = false + wasGetLatestMonthlyPixelCalled = false + setLatestDailyPixel = nil + setLatestWeeklyPixel = nil + setLatestMonthlyPixel = nil + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index dcbc31a911..2036ac9a1d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -111,7 +111,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnOldVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -129,7 +129,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnNewVersionBroker = true sut.checkForUpdatesInBrokerJSONFiles() @@ -146,7 +146,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { if let vault = self.vault { let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil - resources.brokersList = [.init(id: 1, name: "Broker", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] + resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] vault.profileQueries = [.mock] sut.checkForUpdatesInBrokerJSONFiles() diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift index cc23589ec8..c82680d1e9 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift @@ -41,7 +41,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - _ = try await sut.getEmail(dataBrokerName: "fakeBroker") + _ = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTFail("Expected an error to be thrown") } catch { if let error = error as? EmailError, @@ -62,7 +62,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - _ = try await sut.getEmail(dataBrokerName: "fakeBroker") + _ = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTFail("Expected an error to be thrown") } catch { if let error = error as? EmailError, case .cantFindEmail = error { @@ -81,7 +81,7 @@ final class EmailServiceTests: XCTestCase { let sut = EmailService(urlSession: mockURLSession, redeemUseCase: MockRedeemUseCase()) do { - let email = try await sut.getEmail(dataBrokerName: "fakeBroker") + let email = try await sut.getEmail(dataBrokerURL: "fakeBroker") XCTAssertEqual("test@ddg.com", email) } catch { XCTFail("Unexpected. It should not throw") diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift index 4d1ff6b8f8..c59ee80486 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MapperToUITests.swift @@ -140,9 +140,9 @@ final class MapperToUITests: XCTestCase { func testLastScans_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #2", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -153,9 +153,9 @@ final class MapperToUITests: XCTestCase { func testNextScans_areMappedCorrectly() { let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #2", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -165,7 +165,7 @@ final class MapperToUITests: XCTestCase { } func testWhenMirrorSiteIsNotInRemovedPeriod_thenItShouldBeAddedToTotalScans() { - let brokerProfileQueryWithMirrorSite: BrokerProfileQueryData = .mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)]) + let brokerProfileQueryWithMirrorSite: BrokerProfileQueryData = .mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: nil)]) let brokerProfileQueryData: [BrokerProfileQueryData] = [ brokerProfileQueryWithMirrorSite, brokerProfileQueryWithMirrorSite, @@ -178,7 +178,7 @@ final class MapperToUITests: XCTestCase { } func testWhenMirrorSiteIsInRemovedPeriod_thenItShouldNotBeAddedToTotalScans() { - let brokerWithMirrorSiteThatWasRemoved = BrokerProfileQueryData.mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)]) + let brokerWithMirrorSiteThatWasRemoved = BrokerProfileQueryData.mock(dataBrokerName: "Broker #1", mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)]) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(dataBrokerName: "Broker #1"), brokerWithMirrorSiteThatWasRemoved, .mock(dataBrokerName: "Broker #2")] let result = sut.initialScanState(brokerProfileQueryData) @@ -190,7 +190,7 @@ final class MapperToUITests: XCTestCase { let brokerWithMirrorSiteNotRemovedAndWithScan = BrokerProfileQueryData.mock( dataBrokerName: "Broker #1", lastRunDate: Date(), - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)] + mirrorSites: [.init(name: "mirror", url: "mirror.com", addedAt: Date(), removedAt: nil)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [ brokerWithMirrorSiteNotRemovedAndWithScan, @@ -207,7 +207,7 @@ final class MapperToUITests: XCTestCase { let brokerWithMirrorSiteRemovedAndWithScan = BrokerProfileQueryData.mock( dataBrokerName: "Broker #2", lastRunDate: Date(), - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(dataBrokerName: "Broker #1"), @@ -223,7 +223,7 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsNotInRemovedPeriod_thenMatchIsAdded() { let brokerWithMirrorSiteNotRemovedAndWithMatch = BrokerProfileQueryData.mock( extractedProfile: .mockWithoutRemovedDate, - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: nil)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: nil)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteNotRemovedAndWithMatch] @@ -235,7 +235,7 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteIsInRemovedPeriod_thenMatchIsNotAdded() { let brokerWithMirrorSiteRemovedAndWithMatch = BrokerProfileQueryData.mock( extractedProfile: .mockWithoutRemovedDate, - mirrorSites: [.init(name: "mirror", addedAt: Date(), removedAt: Date().yesterday)] + mirrorSites: [.init(name: "mirror", url: "mirror1.com", addedAt: Date(), removedAt: Date().yesterday)] ) let brokerProfileQueryData: [BrokerProfileQueryData] = [.mock(), .mock(), brokerWithMirrorSiteRemovedAndWithMatch] @@ -246,8 +246,8 @@ final class MapperToUITests: XCTestCase { func testMirrorSites_areCorrectlyMappedToInProgressOptOuts() { let scanHistoryEventsWithMatchesFound: [HistoryEvent] = [.init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date())] - let mirrorSiteNotRemoved = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let mirrorSiteRemoved = MirrorSite(name: "mirror #2", addedAt: Date.distantPast, removedAt: Date().yesterday) // Should not be added + let mirrorSiteNotRemoved = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let mirrorSiteRemoved = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date.distantPast, removedAt: Date().yesterday) // Should not be added let brokerProfileQueryData: [BrokerProfileQueryData] = [ .mock(extractedProfile: .mockWithoutRemovedDate, scanHistoryEvents: scanHistoryEventsWithMatchesFound, @@ -261,10 +261,10 @@ final class MapperToUITests: XCTestCase { func testWhenMirrorSiteRemovedIsInRangeToPastRemovedProfile_thenIsAddedToCompletedOptOuts() { let scanHistoryEventsWithMatchesFound: [HistoryEvent] = [.init(brokerId: 1, profileQueryId: 1, type: .matchesFound(count: 1), date: Date().yesterday!)] - let mirrorSiteRemoved = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: Date()) // Should be added + let mirrorSiteRemoved = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: Date()) // Should be added // The next two mirror sites should not be added. New mirror sites should not count for old opt-outs - let newMirrorSiteOne = MirrorSite(name: "mirror #2", addedAt: Date(), removedAt: nil) - let newMirrorSiteTwo = MirrorSite(name: "mirror #3", addedAt: Date(), removedAt: nil) + let newMirrorSiteOne = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date(), removedAt: nil) + let newMirrorSiteTwo = MirrorSite(name: "mirror #3", url: "mirror3.com", addedAt: Date(), removedAt: nil) let brokerProfileQuery = BrokerProfileQueryData.mock(extractedProfile: .mockWithRemoveDate(Date().yesterday!), scanHistoryEvents: scanHistoryEventsWithMatchesFound, mirrorSites: [mirrorSiteRemoved, newMirrorSiteOne, newMirrorSiteTwo]) @@ -276,12 +276,12 @@ final class MapperToUITests: XCTestCase { } func testLastScansWithMirrorSites_areMappedCorrectly() { - let includedMirrorSite = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let notIncludedMirrorSite = MirrorSite(name: "mirror #2", addedAt: Date(), removedAt: nil) + let includedMirrorSite = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let notIncludedMirrorSite = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date(), removedAt: nil) let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", lastRunDate: Date().yesterday, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), - .mock(dataBrokerName: "Broker #2", lastRunDate: Date().yesterday), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", lastRunDate: Date().yesterday, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", lastRunDate: Date().yesterday), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) @@ -291,12 +291,12 @@ final class MapperToUITests: XCTestCase { } func testNextScansWithMirrorSites_areMappedCorrectly() { - let includedMirrorSite = MirrorSite(name: "mirror #1", addedAt: Date.distantPast, removedAt: nil) - let notIncludedMirrorSite = MirrorSite(name: "mirror #2", addedAt: Date.distantPast, removedAt: Date()) + let includedMirrorSite = MirrorSite(name: "mirror #1", url: "mirror1.com", addedAt: Date.distantPast, removedAt: nil) + let notIncludedMirrorSite = MirrorSite(name: "mirror #2", url: "mirror2.com", addedAt: Date.distantPast, removedAt: Date()) let brokerProfileQueryData: [BrokerProfileQueryData] = [ - .mock(dataBrokerName: "Broker #1", preferredRunDate: Date().tomorrow, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), - .mock(dataBrokerName: "Broker #2", preferredRunDate: Date().tomorrow), - .mock(dataBrokerName: "Broker #3") + .mock(dataBrokerName: "Broker #1", url: "broker1.com", preferredRunDate: Date().tomorrow, mirrorSites: [includedMirrorSite, notIncludedMirrorSite]), + .mock(dataBrokerName: "Broker #2", url: "broker2.com", preferredRunDate: Date().tomorrow), + .mock(dataBrokerName: "Broker #3", url: "broker3.com") ] let result = sut.maintenanceScanState(brokerProfileQueryData) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index 8585792744..db0607d891 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -47,7 +47,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelFired! + let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! let pixelName = DataBrokerProtectionPixels.parentChildMatches(parent: "", child: "", value: 0).name XCTAssertEqual(lastPixel.name, pixelName) XCTAssertEqual(Int((lastPixel.params?["value"])!), @@ -72,7 +72,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelFired! + let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! let pixelName = DataBrokerProtectionPixels.parentChildMatches(parent: "", child: "", value: 0).name XCTAssertEqual(lastPixel.name, pixelName) XCTAssertEqual(Int((lastPixel.params?["value"])!), @@ -97,7 +97,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelFired! + let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! let pixelName = DataBrokerProtectionPixels.parentChildMatches(parent: "", child: "", value: 0).name XCTAssertEqual(lastPixel.name, pixelName) XCTAssertEqual(Int((lastPixel.params?["value"])!), @@ -122,7 +122,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelFired! + let lastPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! let pixelName = DataBrokerProtectionPixels.parentChildMatches(parent: "", child: "", value: 0).name XCTAssertEqual(lastPixel.name, pixelName) XCTAssertEqual(Int((lastPixel.params?["value"])!), @@ -143,7 +143,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { sut.calculateMismatches() - XCTAssertNil(MockDataBrokerProtectionPixelsHandler.lastPixelFired) + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.isEmpty) } } @@ -152,6 +152,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: "parent", + url: "parent.com", steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock @@ -165,6 +166,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: "child", + url: "child.com", steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index d053404255..575c0cdd17 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -27,6 +27,7 @@ import GRDB extension BrokerProfileQueryData { static func mock(with steps: [Step] = [Step](), dataBrokerName: String = "test", + url: String = "test.com", lastRunDate: Date? = nil, preferredRunDate: Date? = nil, extractedProfile: ExtractedProfile? = nil, @@ -36,6 +37,7 @@ extension BrokerProfileQueryData { BrokerProfileQueryData( dataBroker: DataBroker( name: dataBrokerName, + url: url, steps: steps, version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, @@ -232,7 +234,7 @@ final class EmailServiceMock: EmailServiceProtocol { var shouldThrow: Bool = false - func getEmail(dataBrokerName: String?) async throws -> String { + func getEmail(dataBrokerURL: String?) async throws -> String { if shouldThrow { throw DataBrokerProtectionError.emailError(nil) } @@ -491,9 +493,9 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault func fetchBroker(with name: String) throws -> DataBroker? { if shouldReturnOldVersionBroker { - return .init(id: 1, name: "Broker", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock) } else if shouldReturnNewVersionBroker { - return .init(id: 1, name: "Broker", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) + return .init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock) } return nil @@ -618,11 +620,11 @@ final class DataBrokerProtectionSecureVaultMock: DataBrokerProtectionSecureVault public class MockDataBrokerProtectionPixelsHandler: EventMapping { - static var lastPixelFired: DataBrokerProtectionPixels? + static var lastPixelsFired = [DataBrokerProtectionPixels]() public init() { super.init { event, _, _, _ in - MockDataBrokerProtectionPixelsHandler.lastPixelFired = event + MockDataBrokerProtectionPixelsHandler.lastPixelsFired.append(event) } } @@ -631,7 +633,7 @@ public class MockDataBrokerProtectionPixelsHandler: EventMapping DataBrokerProtectionProfile? { wasFetchProfileCalled = true - return nil + return profile + } + + func setFetchedProfile(_ profile: DataBrokerProtectionProfile?) { + self.profile = profile } func deleteProfileData() { @@ -800,6 +807,7 @@ final class MockDatabase: DataBrokerProtectionRepository { lastParentBrokerWhereChildSitesWhereFetched = nil lastProfileQueryIdOnScanUpdatePreferredRunDate = nil brokerProfileQueryDataToReturn.removeAll() + profile = nil } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift index b9472a9ab9..369f80ee32 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift @@ -35,6 +35,7 @@ final class OperationPreferredDateUpdaterTests: XCTestCase { let childBroker = DataBroker( id: 1, name: "Child broker", + url: "childbroker.com", steps: [Step](), version: "1.0", schedulingConfig: DataBrokerScheduleConfig( diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index bbe7e894b8..b7b7bb72d9 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -13,7 +13,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/NetworkProtectionMac/Package.resolved b/LocalPackages/NetworkProtectionMac/Package.resolved new file mode 100644 index 0000000000..08c5add0f4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "bloom_cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/bloom_cpp.git", + "state" : { + "revision" : "8076199456290b61b4544bf2f4caf296759906a0", + "version" : "3.0.0" + } + }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "revision" : "1f7932fe67a0d8b1ae97e62cb333639353d4772f", + "version" : "101.2.2" + } + }, + { + "identity" : "content-scope-scripts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/content-scope-scripts", + "state" : { + "revision" : "0b68b0d404d8d4f32296cd84fa160b18b0aeaf44", + "version" : "4.59.1" + } + }, + { + "identity" : "duckduckgo-autofill", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", + "state" : { + "revision" : "b972bc0ab6ee1d57a0a18a197dcc31e40ae6ac57", + "version" : "10.0.3" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/GRDB.swift.git", + "state" : { + "revision" : "9f049d7b97b1e68ffd86744b500660d34a9e79b8", + "version" : "2.3.0" + } + }, + { + "identity" : "privacy-dashboard", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/privacy-dashboard", + "state" : { + "revision" : "38336a574e13090764ba09a6b877d15ee514e371", + "version" : "3.1.1" + } + }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "4356ec54e073741449640d3d50a1fd24fd1e1b8b", + "version" : "2.1.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, + { + "identity" : "sync_crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/sync_crypto", + "state" : { + "revision" : "2ab6ab6f0f96b259c14c2de3fc948935fc16ac78", + "version" : "0.2.0" + } + }, + { + "identity" : "trackerradarkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "state" : { + "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", + "version" : "1.2.2" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/wireguard-apple", + "state" : { + "revision" : "2d8172c11478ab11b0f5ad49bdb4f93f4b3d5e0d", + "version" : "1.1.1" + } + } + ], + "version" : 2 +} diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a1edbe7388..f1e2140732 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -27,10 +27,11 @@ let package = Package( ], products: [ .library(name: "NetworkProtectionIPC", targets: ["NetworkProtectionIPC"]), + .library(name: "NetworkProtectionProxy", targets: ["NetworkProtectionProxy"]), .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems") @@ -50,6 +51,19 @@ let package = Package( plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] ), + // MARK: - NetworkProtectionProxy + + .target( + name: "NetworkProtectionProxy", + dependencies: [ + .product(name: "NetworkProtection", package: "BrowserServicesKit") + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin", package: "BrowserServicesKit")] + ), + // MARK: - NetworkProtectionUI .target( diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift new file mode 100644 index 0000000000..882eb19734 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift @@ -0,0 +1,242 @@ +// +// TCPFlowManager.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 +import NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct TCPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +@TCPFlowActor +enum RemoteConnectionError: Error { + case complete + case cancelled + case couldNotEstablishConnection(_ error: Error) + case unhandledError(_ error: Error) +} + +final class TCPFlowManager { + private let flow: NEAppProxyTCPFlow + private var connectionTask: Task? + private var connection: NWConnection? + + init(flow: NEAppProxyTCPFlow) { + self.flow = flow + } + + deinit { + // Just making extra sure we don't have any unexpected retain cycle + connection?.stateUpdateHandler = nil + connection?.cancel() + } + + func start(interface: NWInterface) async throws { + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint else { + return + } + + try await connectAndStartRunLoop(remoteEndpoint: remoteEndpoint, interface: interface) + } + + private func connectAndStartRunLoop(remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws { + let remoteConnection = try await connect(to: remoteEndpoint, interface: interface) + try await flow.open(withLocalEndpoint: nil) + + do { + try await startDataCopyLoop(for: remoteConnection) + + remoteConnection.cancel() + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + remoteConnection.cancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface) async throws -> NWConnection { + try await withCheckedThrowingContinuation { continuation in + connect(to: remoteEndpoint, interface: interface) { result in + switch result { + case .success(let connection): + continuation.resume(returning: connection) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + func connect(to remoteEndpoint: NWHostEndpoint, interface: NWInterface, completion: @escaping @TCPFlowActor (Result) -> Void) { + let host = Network.NWEndpoint.Host(remoteEndpoint.hostname) + let port = Network.NWEndpoint.Port(remoteEndpoint.port)! + + let parameters = NWParameters.tcp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + self.connection = connection + + connection.stateUpdateHandler = { (state: NWConnection.State) in + Task { @TCPFlowActor in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(connection)) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + } + + connection.start(queue: .global()) + } + + private func startDataCopyLoop(for remoteConnection: NWConnection) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyOutboundTraffic(to: remoteConnection) + } + } + + group.addTask { [weak self] in + while true { + guard let self else { + throw RemoteConnectionError.cancelled + } + + try Task.checkCancellation() + try await self.copyInboundTraffic(from: remoteConnection) + } + } + + while !group.isEmpty { + do { + try await group.next() + + } catch { + group.cancelAll() + throw error + } + } + } + } + + @MainActor + func closeFlow(remoteConnection: NWConnection, error: Error?) { + remoteConnection.forceCancel() + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + + static let maxReceiveSize: Int = Int(Measurement(value: 2, unit: UnitInformationStorage.megabytes).converted(to: .bytes).value) + + func copyInboundTraffic(from remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + remoteConnection.receive(minimumIncompleteLength: 1, + maximumLength: Self.maxReceiveSize) { [weak flow] (data, _, isComplete, error) in + guard let flow else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (.some(let data), _, _) where !data.isEmpty: + flow.write(data) { writeError in + if let writeError { + continuation.resume(throwing: writeError) + remoteConnection.cancel() + } else { + continuation.resume() + } + } + case (_, isComplete, _) where isComplete == true: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + case (_, _, .some(let error)): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } + + func copyOutboundTraffic(to remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @TCPFlowActor in + flow.readData { data, error in + switch (data, error) { + case (.some(let data), _) where !data.isEmpty: + remoteConnection.send(content: data, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + remoteConnection.cancel() + return + } + + continuation.resume() + })) + case (_, .some(let error)): + continuation.resume(throwing: error) + remoteConnection.cancel() + default: + continuation.resume(throwing: RemoteConnectionError.complete) + remoteConnection.cancel() + } + } + } + } + } +} + +extension TCPFlowManager: Hashable { + static func == (lhs: TCPFlowManager, rhs: TCPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift new file mode 100644 index 0000000000..000f37d20e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/UDPFlowManager.swift @@ -0,0 +1,329 @@ +// +// UDPFlowManager.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 +import NetworkExtension +import OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// A private global actor to handle UDP flows management +/// +@globalActor +struct UDPFlowActor { + actor ActorType { } + + static let shared: ActorType = ActorType() +} + +/// Class to handle UDP connections +/// +/// This is necessary because as described in the reference comment for this implementation (see ``UDPFlowManager``'s documentation) +/// it's noted that a single UDP flow can have to manage multiple connections. +/// +@UDPFlowActor +final class UDPConnectionManager { + let endpoint: NWEndpoint + private let connection: NWConnection + private let onReceive: (_ endpoint: NWEndpoint, _ result: Result) async -> Void + + init(endpoint: NWHostEndpoint, interface: NWInterface?, onReceive: @UDPFlowActor @escaping (_ endpoint: NWEndpoint, _ result: Result) async -> Void) { + let host = Network.NWEndpoint.Host(endpoint.hostname) + let port = Network.NWEndpoint.Port(endpoint.port)! + + let parameters = NWParameters.udp + parameters.preferNoProxies = true + parameters.requiredInterface = interface + parameters.prohibitedInterfaceTypes = [.other] + + let connection = NWConnection(host: host, port: port, using: parameters) + + self.connection = connection + self.endpoint = endpoint + self.onReceive = onReceive + } + + deinit { + // Just making extra sure we don't retain anything we don't need to + connection.stateUpdateHandler = nil + connection.cancel() + } + + // MARK: - General Operation + + /// Starts the operation of this connection manager + /// + /// Can be called multiple times safely. + /// + private func start() async throws { + guard connection.state == .setup else { + return + } + + try await connect() + + Task { + while true { + do { + let datagram = try await receive() + await onReceive(endpoint, .success(datagram)) + } catch { + connection.cancel() + await onReceive(endpoint, .failure(error)) + break + } + } + } + } + + // MARK: - Connection Management + + private func connect() async throws { + try await withCheckedThrowingContinuation { continuation in + connect { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + private func connect(completion: @escaping (Result) -> Void) { + connection.stateUpdateHandler = { [connection] (state: NWConnection.State) in + switch state { + case .ready: + connection.stateUpdateHandler = nil + completion(.success(())) + case .cancelled: + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.cancelled)) + case .failed(let error), .waiting(let error): + connection.stateUpdateHandler = nil + completion(.failure(RemoteConnectionError.couldNotEstablishConnection(error))) + default: + break + } + } + + connection.start(queue: .global()) + } + + // MARK: - Receiving from remote + + private func receive() async throws -> Data { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.receiveMessage { [weak self] data, _, isComplete, error in + + guard self != nil else { + continuation.resume(throwing: RemoteConnectionError.cancelled) + return + } + + switch (data, isComplete, error) { + case (let data?, _, _): + continuation.resume(returning: data) + case (_, true, _): + continuation.resume(throwing: RemoteConnectionError.cancelled) + case (_, _, let error?): + continuation.resume(throwing: RemoteConnectionError.unhandledError(error)) + default: + continuation.resume(throwing: RemoteConnectionError.cancelled) + } + } + } + } + + // MARK: - Writing datagrams + + func write(datagram: Data) async throws { + try await start() + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPConnectionManager: Hashable, Equatable { + // MARK: - Equatable + + static func == (lhs: UDPConnectionManager, rhs: UDPConnectionManager) -> Bool { + lhs.endpoint == rhs.endpoint + } + + // MARK: - Hashable + + func hash(into hasher: inout Hasher) { + hasher.combine(endpoint) + } +} + +/// UDP flow manager class +/// +/// There is documentation explaining how to handle TCP flows here: +/// https://developer.apple.com/documentation/networkextension/app_proxy_provider/handling_flow_copying?changes=_8 +/// +/// Unfortunately there isn't good official documentation showcasing how to implement UDP flow management. +/// The best we could fine are two comments by an Apple engineer that shine some light on how that implementation should be like: +/// https://developer.apple.com/forums/thread/678464?answerId=671531022#671531022 +/// https://developer.apple.com/forums/thread/678464?answerId=671892022#671892022 +/// +/// This class is the result of implementing the description found in that comment. +/// +@UDPFlowActor +final class UDPFlowManager { + private let flow: NEAppProxyUDPFlow + private var interface: NWInterface? + + private var connectionManagers = [NWEndpoint: UDPConnectionManager]() + + init(flow: NEAppProxyUDPFlow) { + self.flow = flow + } + + func start(interface: NWInterface) async throws { + self.interface = interface + try await connectAndStartRunLoop() + } + + private func connectAndStartRunLoop() async throws { + do { + try await flow.open(withLocalEndpoint: nil) + try await startDataCopyLoop() + + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } catch { + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + } + } + + private func startDataCopyLoop() async throws { + while true { + try await copyOutoundTraffic() + } + } + + func copyInboundTraffic(endpoint: NWEndpoint, result: Result) async { + switch result { + case .success(let data): + do { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + flow.writeDatagrams([data], sentBy: [endpoint]) { error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + } + } + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + case .failure: + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + + func copyOutoundTraffic() async throws { + let (datagrams, endpoints) = try await read() + + // Ref: https://developer.apple.com/documentation/networkextension/neappproxyudpflow/1406576-readdatagrams + if datagrams.isEmpty || endpoints.isEmpty { + throw NEAppProxyFlowError(.aborted) + } + + for (datagram, endpoint) in zip(datagrams, endpoints) { + guard let endpoint = endpoint as? NWHostEndpoint else { + // Not sure what to do about this... + continue + } + + let manager = connectionManagers[endpoint] ?? { + let manager = UDPConnectionManager(endpoint: endpoint, interface: interface, onReceive: copyInboundTraffic) + connectionManagers[endpoint] = manager + return manager + }() + + do { + try await manager.write(datagram: datagram) + } catch { + // Any failure means we close the connection + connectionManagers.removeValue(forKey: endpoint) + } + } + } + + /// Reads datagrams from the flow. + /// + /// Apple's documentation is very bad here, but it seems each datagram is corresponded with an endpoint at the same position in the array + /// as mentioned here: https://developer.apple.com/forums/thread/75893 + /// + private func read() async throws -> (datagrams: [Data], endpoints: [NWEndpoint]) { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<([Data], [NWEndpoint]), Error>) in + flow.readDatagrams { datagrams, endpoints, error in + if let error { + continuation.resume(throwing: error) + return + } + + guard let datagrams, let endpoints else { + continuation.resume(throwing: NEAppProxyFlowError(.aborted)) + return + } + + continuation.resume(returning: (datagrams, endpoints)) + } + } + } + + private func send(datagram: Data, through remoteConnection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + remoteConnection.send(content: datagram, completion: .contentProcessed({ error in + if let error { + continuation.resume(throwing: error) + return + } + + continuation.resume() + })) + } + } +} + +extension UDPFlowManager: Hashable { + static func == (lhs: UDPFlowManager, rhs: UDPFlowManager) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(flow) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift new file mode 100644 index 0000000000..12339a673d --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyAppMessageHandler.swift @@ -0,0 +1,82 @@ +// +// TransparentProxyAppMessageHandler.swift +// +// 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 OSLog // swiftlint:disable:this enforce_os_log_wrapper + +/// Handles app messages +/// +final class TransparentProxyAppMessageHandler { + + private let settings: TransparentProxySettings + + init(settings: TransparentProxySettings) { + self.settings = settings + } + + func handle(_ data: Data) async -> Data? { + do { + let message = try JSONDecoder().decode(TransparentProxyMessage.self, from: data) + return await handle(message) + } catch { + return nil + } + } + + /// Handles a message. + /// + /// This method will wrap the message into a request with a completion handler, and will process it. + /// The reason why this method wraps the message in a request is to ensure that the response + /// type stays syncrhonized between app and provider. + /// + private func handle(_ message: TransparentProxyMessage) async -> Data? { + await withCheckedContinuation { continuation in + var request: TransparentProxyRequest + + switch message { + case .changeSetting(let change): + request = .changeSetting(change, responseHandler: { + continuation.resume(returning: nil) + }) + } + + handle(request) + } + } + + /// Handles a request and calls the response handler when done. + /// + private func handle(_ request: TransparentProxyRequest) { + switch request { + case .changeSetting(let change, let responseHandler): + handle(change) + responseHandler() + } + } + + /// Handles a settings change. + /// + private func handle(_ settingChange: TransparentProxySettings.Change) { + switch settingChange { + case .appRoutingRules(let routingRules): + settings.appRoutingRules = routingRules + case .excludedDomains(let excludedDomains): + settings.excludedDomains = excludedDomains + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift new file mode 100644 index 0000000000..0881dba3b1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/IPC/TransparentProxyRequest.swift @@ -0,0 +1,67 @@ +// +// TransparentProxyRequest.swift +// +// 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 NetworkExtension + +public enum TransparentProxyMessage: Codable { + case changeSetting(_ change: TransparentProxySettings.Change) +} + +/// A request for the TransparentProxyProvider. +/// +/// This enum associates a request with a response handler making XPC communication simpler. +/// Once the request completes, `responseHandler` will be called with the result. +/// +public enum TransparentProxyRequest { + case changeSetting(_ settingChange: TransparentProxySettings.Change, responseHandler: () -> Void) + + var message: TransparentProxyMessage { + switch self { + case .changeSetting(let change, _): + return .changeSetting(change) + } + } + + func handleResponse(data: Data?) { + switch self { + case .changeSetting(_, let handleResponse): + handleResponse() + } + } +} + +/// Respresents a transparent proxy session. +/// +/// Offers basic IPC communication support for the app that owns the proxy. This mechanism +/// is implemented through `NETunnelProviderSession` which means only the app that +/// owns the proxy can use this class. +/// +public class TransparentProxySession { + + private let session: NETunnelProviderSession + + init(_ session: NETunnelProviderSession) { + self.session = session + } + + func send(_ request: TransparentProxyRequest) throws { + let payload = try JSONEncoder().encode(request.message) + try session.sendProviderMessage(payload, responseHandler: request.handleResponse(data:)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift new file mode 100644 index 0000000000..b4c9b8cebb --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift @@ -0,0 +1,89 @@ +// +// TransparentProxyControllerPixel.swift +// +// 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 PixelKit + +extension TransparentProxyController.StartError: PixelKitEventErrorDetails { + public var underlyingError: Error? { + switch self { + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return underlyingError + default: + return nil + } + } +} + +extension TransparentProxyController { + + public enum Event: PixelKitEventV2 { + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + // MARK: - PixelKit.Event + + public var name: String { + namePrefix + "_" + nameSuffix + } + + public var parameters: [String: String]? { + switch self { + case .startInitiated: + return nil + case .startSuccess: + return nil + case .startFailure: + return nil + } + } + + // MARK: - PixelKit Support + + private static let pixelNamePrefix = "vpn_proxy_controller" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var nameSuffix: String { + switch self { + case .startInitiated: + return "start_initiated" + case .startFailure: + return "start_failure" + case .startSuccess: + return "start_success" + } + } + + public var error: Error? { + switch self { + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift new file mode 100644 index 0000000000..aff7421bea --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyProviderPixel.swift @@ -0,0 +1,93 @@ +// +// TransparentProxyProviderPixel.swift +// +// 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 PixelKit + +extension TransparentProxyProvider.StartError: ErrorWithPixelParameters { + public var errorParameters: [String: String] { + switch self { + case .failedToUpdateNetworkSettings(let underlyingError): + return [ + PixelKit.Parameters.underlyingErrorCode: "\((underlyingError as NSError).code)", + PixelKit.Parameters.underlyingErrorDesc: (underlyingError as NSError).domain, + ] + default: + return [:] + } + } +} + +extension TransparentProxyProvider { + + public enum Event: PixelKitEventV2 { + case failedToUpdateNetworkSettings(_ error: Error) + case startInitiated + case startSuccess + case startFailure(_ error: Error) + + private static let pixelNamePrefix = "vpn_proxy_provider" + + private var namePrefix: String { + Self.pixelNamePrefix + } + + private var namePostfix: String { + switch self { + case .failedToUpdateNetworkSettings: + return "failed_to_update_network_settings" + case .startFailure: + return "start_failure" + case .startInitiated: + return "start_initiated" + case .startSuccess: + return "start_success" + } + } + + public var name: String { + namePrefix + "_" + namePostfix + } + + public var parameters: [String: String]? { + switch self { + case.failedToUpdateNetworkSettings: + return nil + case .startFailure: + return nil + case .startInitiated: + return nil + case .startSuccess: + return nil + } + } + + public var error: Error? { + switch self { + case .failedToUpdateNetworkSettings(let error): + return error + case .startInitiated: + return nil + case .startFailure(let error): + return error + case .startSuccess: + return nil + } + } + } +} diff --git a/DuckDuckGoVPN/Bundle+Configuration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift similarity index 56% rename from DuckDuckGoVPN/Bundle+Configuration.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift index 936c44a4a8..15d4a4e1a1 100644 --- a/DuckDuckGoVPN/Bundle+Configuration.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNAppRoutingRules.swift @@ -1,7 +1,7 @@ // -// Bundle+Configuration.swift +// VPNAppRoutingRules.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -18,14 +18,4 @@ import Foundation -extension Bundle { - private static let networkExtensionBundleIDKey = "SYSEX_BUNDLE_ID" - - var networkExtensionBundleID: String { - guard let bundleID = object(forInfoDictionaryKey: Self.networkExtensionBundleIDKey) as? String else { - fatalError("Info.plist is missing \(Self.networkExtensionBundleIDKey)") - } - - return bundleID - } -} +public typealias VPNAppRoutingRules = [String: VPNRoutingRule] diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift similarity index 59% rename from DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift rename to LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift index e90627cbc2..b96a0773cf 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/SystemExtensionAndNotificationTargets/NetworkProtectionExtensionMachService.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/RoutingRules/VPNRoutingRule.swift @@ -1,7 +1,7 @@ // -// NetworkProtectionExtensionMachService.swift +// VPNRoutingRule.swift // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// 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. @@ -18,14 +18,12 @@ import Foundation -/// Helper methods associated with mach services. +/// Routing rules /// -final class NetworkProtectionExtensionMachService { - - /// Retrieves the mach service name from a network extension bundle. - /// - static func serviceName() -> String { - NetworkProtectionBundle.extensionBundle().machServiceName - } - +/// Note that there's no need for an `ignore` case because that's achieved by not having a rule +/// in the first place. +/// +public enum VPNRoutingRule: Codable, Equatable { + case block + case exclude } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift new file mode 100644 index 0000000000..db010ec2b5 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/TransparentProxySettings.swift @@ -0,0 +1,134 @@ +// +// TransparentProxySettings.swift +// +// 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 Combine +import Foundation + +public final class TransparentProxySettings { + public enum Change: Codable { + case appRoutingRules(_ routingRules: VPNAppRoutingRules) + case excludedDomains(_ excludedDomains: [String]) + } + + let defaults: UserDefaults + + private(set) public lazy var changePublisher: AnyPublisher = { + Publishers.MergeMany( + defaults.vpnProxyAppRoutingRulesPublisher + .dropFirst() + .removeDuplicates() + .map { routingRules in + Change.appRoutingRules(routingRules) + }.eraseToAnyPublisher(), + defaults.vpnProxyExcludedDomainsPublisher + .dropFirst() + .removeDuplicates() + .map { excludedDomains in + Change.excludedDomains(excludedDomains) + }.eraseToAnyPublisher() + ).eraseToAnyPublisher() + }() + + public init(defaults: UserDefaults) { + self.defaults = defaults + } + + // MARK: - Settings + + public var appRoutingRules: VPNAppRoutingRules { + get { + defaults.vpnProxyAppRoutingRules + } + + set { + defaults.vpnProxyAppRoutingRules = newValue + } + } + + public var excludedDomains: [String] { + get { + defaults.vpnProxyExcludedDomains + } + + set { + defaults.vpnProxyExcludedDomains = newValue + } + } + + // MARK: - Reset to factory defaults + + public func resetAll() { + defaults.resetVPNProxyAppRoutingRules() + defaults.resetVPNProxyExcludedDomains() + } + + // MARK: - App routing rules logic + + public func isBlocking(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .block + } + + public func isExcluding(_ appIdentifier: String) -> Bool { + appRoutingRules[appIdentifier] == .exclude + } + + public func toggleBlocking(for appIdentifier: String) { + if isBlocking(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .block + } + } + + public func toggleExclusion(for appIdentifier: String) { + if isExcluding(appIdentifier) { + appRoutingRules.removeValue(forKey: appIdentifier) + } else { + appRoutingRules[appIdentifier] = .exclude + } + } + + // MARK: - Snapshot support + + public func snapshot() -> TransparentProxySettingsSnapshot { + .init(appRoutingRules: appRoutingRules, excludedDomains: excludedDomains) + } + + public func apply(_ snapshot: TransparentProxySettingsSnapshot) { + appRoutingRules = snapshot.appRoutingRules + excludedDomains = snapshot.excludedDomains + } +} + +extension TransparentProxySettings: CustomStringConvertible { + public var description: String { + """ + TransparentProxySettings {\n + appRoutingRules: \(appRoutingRules)\n + excludedDomains: \(excludedDomains)\n + } + """ + } +} + +public struct TransparentProxySettingsSnapshot: Codable { + public static let key = "com.duckduckgo.TransparentProxySettingsSnapshot" + + public let appRoutingRules: VPNAppRoutingRules + public let excludedDomains: [String] +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift new file mode 100644 index 0000000000..1090ed1626 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedApps.swift @@ -0,0 +1,79 @@ +// +// UserDefaults+excludedApps.swift +// +// 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 Combine +import Foundation + +extension UserDefaults { + private var vpnProxyAppRoutingRulesDataKey: String { + "vpnProxyAppRoutingRulesData" + } + + @objc + dynamic var vpnProxyAppRoutingRulesData: Data? { + get { + object(forKey: vpnProxyAppRoutingRulesDataKey) as? Data + } + + set { + guard let newValue, + newValue.count > 0 else { + + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + return + } + + set(newValue, forKey: vpnProxyAppRoutingRulesDataKey) + } + } + + var vpnProxyAppRoutingRules: VPNAppRoutingRules { + get { + guard let data = vpnProxyAppRoutingRulesData, + let routingRules = try? JSONDecoder().decode(VPNAppRoutingRules.self, from: data) else { + return [:] + } + + return routingRules + } + + set { + if newValue.isEmpty { + vpnProxyAppRoutingRulesData = nil + return + } + + guard let data = try? JSONEncoder().encode(newValue) else { + vpnProxyAppRoutingRulesData = nil + return + } + + vpnProxyAppRoutingRulesData = data + } + } + + var vpnProxyAppRoutingRulesPublisher: AnyPublisher { + publisher(for: \.vpnProxyAppRoutingRulesData).map { [weak self] _ in + self?.vpnProxyAppRoutingRules ?? [:] + }.eraseToAnyPublisher() + } + + func resetVPNProxyAppRoutingRules() { + removeObject(forKey: vpnProxyAppRoutingRulesDataKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift new file mode 100644 index 0000000000..7500178da7 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Settings/UserDefaultsExtensions/UserDefaults+excludedDomains.swift @@ -0,0 +1,51 @@ +// +// UserDefaults+excludedDomains.swift +// +// 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 Combine +import Foundation + +extension UserDefaults { + private var vpnProxyExcludedDomainsKey: String { + "vpnProxyExcludedDomains" + } + + @objc + dynamic var vpnProxyExcludedDomains: [String] { + get { + object(forKey: vpnProxyExcludedDomainsKey) as? [String] ?? [] + } + + set { + guard newValue.count > 0 else { + + removeObject(forKey: vpnProxyExcludedDomainsKey) + return + } + + set(newValue, forKey: vpnProxyExcludedDomainsKey) + } + } + + var vpnProxyExcludedDomainsPublisher: AnyPublisher<[String], Never> { + publisher(for: \.vpnProxyExcludedDomains).eraseToAnyPublisher() + } + + func resetVPNProxyExcludedDomains() { + removeObject(forKey: vpnProxyExcludedDomainsKey) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift new file mode 100644 index 0000000000..fdc7fb3177 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyController.swift @@ -0,0 +1,293 @@ +// +// TransparentProxyController.swift +// +// 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 Combine +import Foundation +import NetworkExtension +import NetworkProtection +import OSLog // swiftlint:disable:this enforce_os_log_wrapper +import PixelKit +import SystemExtensions + +/// Controller for ``TransparentProxyProvider`` +/// +@MainActor +public final class TransparentProxyController { + + public enum StartError: Error { + case attemptToStartWithoutBackingActiveFeatures + case couldNotRetrieveProtocolConfiguration + case couldNotEncodeSettingsSnapshot + case failedToLoadConfiguration(_ error: Error) + case failedToSaveConfiguration(_ error: Error) + case failedToStartProvider(_ error: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias ManagerSetupCallback = (_ manager: NETransparentProxyManager) async -> Void + + /// Dry mode means this won't really do anything to start or stop the proxy. + /// + /// This is useful for testing. + /// + private let dryMode: Bool + + /// The bundleID of the extension that contains the ``TransparentProxyProvider``. + /// + private let extensionID: String + + /// The event handler + /// + public var eventHandler: EventCallback? + + /// Callback to set up a ``NETransparentProxyManager``. + /// + public let setup: ManagerSetupCallback + + private var internalManager: NETransparentProxyManager? + + /// Whether the proxy settings should be stored in the provider configuration. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + private let storeSettingsInProviderConfiguration: Bool + public let settings: TransparentProxySettings + private let notificationCenter: NotificationCenter + private var cancellables = Set() + + // MARK: - Initializers + + /// Default initializer. + /// + /// - Parameters: + /// - extensionID: the bundleID for the extension that contains the ``TransparentProxyProvider``. + /// This class DOES NOT take any responsibility in installing the system extension. It only uses + /// the extensionID to identify the appropriate manager configuration to load / save. + /// - storeSettingsInProviderConfiguration: whether the provider configuration will be used for storing + /// the proxy settings. Should be `true` when using a System Extension and `false` when using + /// an App Extension. + /// - settings: the settings to use for this proxy. + /// - dryMode: whether this class is initialized in dry mode. + /// - setup: a callback that will be called whenever a ``NETransparentProxyManager`` needs + /// to be setup. + /// + public init(extensionID: String, + storeSettingsInProviderConfiguration: Bool, + settings: TransparentProxySettings, + notificationCenter: NotificationCenter = .default, + dryMode: Bool = false, + setup: @escaping ManagerSetupCallback) { + + self.dryMode = dryMode + self.extensionID = extensionID + self.notificationCenter = notificationCenter + self.settings = settings + self.setup = setup + self.storeSettingsInProviderConfiguration = storeSettingsInProviderConfiguration + + subscribeToProviderConfigurationChanges() + subscribeToSettingsChanges() + } + + // MARK: - Relay Settings Changes + + private func subscribeToProviderConfigurationChanges() { + notificationCenter.publisher(for: .NEVPNConfigurationChange) + .receive(on: DispatchQueue.main) + .sink { _ in + self.reloadProviderConfiguration() + } + .store(in: &cancellables) + } + + private func reloadProviderConfiguration() { + Task { @MainActor in + try? await self.manager?.loadFromPreferences() + } + } + + private func subscribeToSettingsChanges() { + settings.changePublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: relay(_:)) + .store(in: &cancellables) + } + + private func relay(_ change: TransparentProxySettings.Change) { + Task { @MainActor in + guard let session = await session else { + return + } + + switch session.status { + case .connected, .connecting, .reasserting: + break + default: + return + } + + try TransparentProxySession(session).send(.changeSetting(change, responseHandler: { + // no-op + })) + } + } + + // MARK: - Setting up NETransparentProxyManager + + /// Loads a saved manager + /// + /// This is a bit of a hack that will be run just once for the instance. The reason we want this to run only once is that + /// `NETransparentProxyManager.loadAllFromPreferences()` has a bug where it triggers status change + /// notifications. If the code trying to retrieve the manager is the result of a notification, we may soon find outselves + /// in an infinite loop. + /// + private var triedLoadingManager = false + + /// Loads the configuration matching our ``extensionID``. + /// + public var manager: NETransparentProxyManager? { + get async { + if let internalManager { + return internalManager + } + + if !triedLoadingManager { + triedLoadingManager = true + + let manager = try? await NETransparentProxyManager.loadAllFromPreferences().first { manager in + (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == extensionID + } + self.internalManager = manager + } + + return internalManager + } + } + + /// Loads an existing configuration or creates a new one, if one doesn't exist. + /// + /// - Returns a properly configured `NETransparentProxyManager`. + /// + public func loadOrCreateConfiguration() async throws -> NETransparentProxyManager { + let manager = await manager ?? { + let manager = NETransparentProxyManager() + internalManager = manager + return manager + }() + + await setup(manager) + try setupAdditionalProviderConfiguration(manager) + + try await manager.saveToPreferences() + try await manager.loadFromPreferences() + + return manager + } + + private func setupAdditionalProviderConfiguration(_ manager: NETransparentProxyManager) throws { + guard storeSettingsInProviderConfiguration else { + return + } + + guard let providerProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol else { + throw StartError.couldNotRetrieveProtocolConfiguration + } + + var providerConfiguration = providerProtocol.providerConfiguration ?? [String: Any]() + + guard let encodedSettings = try? JSONEncoder().encode(settings.snapshot()), + let encodedSettingsString = String(data: encodedSettings, encoding: .utf8) else { + + throw StartError.couldNotEncodeSettingsSnapshot + } + + providerConfiguration[TransparentProxySettingsSnapshot.key] = encodedSettingsString as NSString + providerProtocol.providerConfiguration = providerConfiguration + + } + + // MARK: - Connection & Session + + public var connection: NEVPNConnection? { + get async { + await manager?.connection + } + } + + public var session: NETunnelProviderSession? { + get async { + guard let manager = await manager, + 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 + } + } + + // MARK: - Connection + + public var status: NEVPNStatus { + get async { + await connection?.status ?? .disconnected + } + } + + // MARK: - Start & stop the proxy + + public var isRequiredForActiveFeatures: Bool { + settings.appRoutingRules.count > 0 || settings.excludedDomains.count > 0 + } + + public func start() async throws { + eventHandler?(.startInitiated) + + guard isRequiredForActiveFeatures else { + let error = StartError.attemptToStartWithoutBackingActiveFeatures + eventHandler?(.startFailure(error)) + throw error + } + + let manager: NETransparentProxyManager + + do { + manager = try await loadOrCreateConfiguration() + } catch { + eventHandler?(.startFailure(error)) + throw error + } + + do { + try manager.connection.startVPNTunnel(options: [:]) + } catch { + let error = StartError.failedToStartProvider(error) + eventHandler?(.startFailure(error)) + throw error + } + + eventHandler?(.startSuccess) + } + + public func stop() async { + await connection?.stopVPNTunnel() + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift new file mode 100644 index 0000000000..33b75fd73b --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProvider.swift @@ -0,0 +1,389 @@ +// +// TransparentProxyProvider.swift +// +// 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 NetworkExtension +import NetworkProtection +import os.log // swiftlint:disable:this enforce_os_log_wrapper +import SystemConfiguration + +open class TransparentProxyProvider: NETransparentProxyProvider { + + public enum StartError: Error { + case missingProviderConfiguration + case failedToUpdateNetworkSettings(underlyingError: Error) + } + + public typealias EventCallback = (Event) -> Void + public typealias LoadOptionsCallback = (_ options: [String: Any]?) throws -> Void + + static let dnsPort = 53 + + @TCPFlowActor + var tcpFlowManagers = Set() + + @UDPFlowActor + var udpFlowManagers = Set() + + private let monitor = nw_path_monitor_create() + var directInterface: nw_interface_t? + + private let bMonitor = NWPathMonitor() + var interface: NWInterface? + + public let configuration: Configuration + public let settings: TransparentProxySettings + + @MainActor + public var isRunning = false + + public var eventHandler: EventCallback? + private let logger: Logger + + private lazy var appMessageHandler = TransparentProxyAppMessageHandler(settings: settings) + + // MARK: - Init + + public init(settings: TransparentProxySettings, + configuration: Configuration, + logger: Logger) { + + self.configuration = configuration + self.logger = logger + self.settings = settings + + logger.debug("[+] \(String(describing: Self.self), privacy: .public)") + } + + deinit { + logger.debug("[-] \(String(describing: Self.self), privacy: .public)") + } + + private func loadProviderConfiguration() throws { + guard configuration.loadSettingsFromProviderConfiguration else { + return + } + + guard let providerConfiguration = (protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration, + let encodedSettingsString = providerConfiguration[TransparentProxySettingsSnapshot.key] as? String, + let encodedSettings = encodedSettingsString.data(using: .utf8) else { + + throw StartError.missingProviderConfiguration + } + + let snapshot = try JSONDecoder().decode(TransparentProxySettingsSnapshot.self, from: encodedSettings) + settings.apply(snapshot) + } + + @MainActor + public func updateNetworkSettings() async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Task { @MainActor in + let networkSettings = makeNetworkSettings() + logger.log("Updating network settings: \(String(describing: networkSettings), privacy: .public)") + + setTunnelNetworkSettings(networkSettings) { [eventHandler, logger] error in + if let error { + logger.error("Failed to update network settings: \(String(describing: error), privacy: .public)") + eventHandler?(.failedToUpdateNetworkSettings(error)) + continuation.resume(throwing: error) + return + } + + logger.log("Successfully Updated network settings: \(String(describing: error), privacy: .public))") + continuation.resume() + } + } + } + } + + private func makeNetworkSettings() -> NETransparentProxyNetworkSettings { + let networkSettings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + + networkSettings.includedNetworkRules = [ + NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "127.0.0.1", port: ""), remotePrefix: 0, localNetwork: nil, localPrefix: 0, protocol: .any, direction: .outbound) + ] + + return networkSettings + } + + override public func startProxy(options: [String: Any]?, + completionHandler: @escaping (Error?) -> Void) { + + eventHandler?(.startInitiated) + + logger.log( + """ + Starting proxy\n + > configuration: \(String(describing: self.configuration), privacy: .public)\n + > settings: \(String(describing: self.settings), privacy: .public)\n + > options: \(String(describing: options), privacy: .public) + """) + + do { + try loadProviderConfiguration() + } catch { + logger.error("Failed to load provider configuration, bailing out") + eventHandler?(.startFailure(error)) + completionHandler(error) + return + } + + Task { @MainActor in + do { + startMonitoringNetworkInterfaces() + + try await updateNetworkSettings() + logger.log("Proxy started successfully") + isRunning = true + eventHandler?(.startSuccess) + completionHandler(nil) + } catch { + let error = StartError.failedToUpdateNetworkSettings(underlyingError: error) + logger.error("Proxy failed to start \(String(reflecting: error), privacy: .public)") + eventHandler?(.startFailure(error)) + completionHandler(error) + } + } + } + + override public func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + logger.log("Stopping proxy with reason: \(String(reflecting: reason), privacy: .public)") + + Task { @MainActor in + stopMonitoringNetworkInterfaces() + isRunning = false + completionHandler() + } + } + + override public func sleep(completionHandler: @escaping () -> Void) { + Task { @MainActor in + stopMonitoringNetworkInterfaces() + logger.log("The proxy is now sleeping") + completionHandler() + } + } + + override public func wake() { + Task { @MainActor in + logger.log("The proxy is now awake") + startMonitoringNetworkInterfaces() + } + } + + override public func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { + guard let flow = flow as? NEAppProxyTCPFlow else { + logger.info("Expected a TCP flow, but got something else. We're ignoring it.") + return false + } + + guard let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = flow.remoteHostname ?? (flow.remoteEndpoint as? NWHostEndpoint)?.hostname ?? "unknown" + + logger.debug( + """ + [TCP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[TCP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[TCP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @TCPFlowActor in + let flowManager = TCPFlowManager(flow: flow) + tcpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + tcpFlowManagers.remove(flowManager) + } + + return true + } + + override public func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow, initialRemoteEndpoint remoteEndpoint: NWEndpoint) -> Bool { + + guard let remoteEndpoint = remoteEndpoint as? NWHostEndpoint, + !isDnsServer(remoteEndpoint) else { + return false + } + + let printableRemote = remoteEndpoint.hostname + + logger.log( + """ + [UDP] New flow: \(String(describing: flow), privacy: .public) + - remote: \(printableRemote, privacy: .public) + - flowID: \(String(describing: flow.metaData.filterFlowIdentifier?.uuidString), privacy: .public) + - appID: \(String(describing: flow.metaData.sourceAppSigningIdentifier), privacy: .public) + """) + + guard let interface else { + logger.error("[UDP: \(String(describing: flow), privacy: .public)] Expected an interface to exclude traffic through") + return false + } + + switch path(for: flow) { + case .block(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Blocking traffic due to domain rule") + } + case .excludeFromVPN(let reason): + switch reason { + case .appRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to app rule") + case .domainRule: + logger.debug("[UDP: \(String(describing: flow), privacy: .public)] Excluding traffic due to domain rule") + } + case .routeThroughVPN: + return false + } + + flow.networkInterface = directInterface + + Task { @UDPFlowActor in + let flowManager = UDPFlowManager(flow: flow) + udpFlowManagers.insert(flowManager) + + try? await flowManager.start(interface: interface) + udpFlowManagers.remove(flowManager) + } + + return true + } + + // MARK: - Path Monitors + + @MainActor + private func startMonitoringNetworkInterfaces() { + bMonitor.pathUpdateHandler = { [weak self, logger] path in + logger.log("Available interfaces updated: \(String(reflecting: path.availableInterfaces), privacy: .public)") + + self?.interface = path.availableInterfaces.first { interface in + interface.type != .other + } + } + bMonitor.start(queue: .main) + + nw_path_monitor_set_queue(monitor, .main) + nw_path_monitor_set_update_handler(monitor) { [weak self, logger] path in + guard let self else { return } + + let interfaces = SCNetworkInterfaceCopyAll() + logger.log("Available interfaces updated: \(String(reflecting: interfaces), privacy: .public)") + + nw_path_enumerate_interfaces(path) { interface in + guard nw_interface_get_type(interface) != nw_interface_type_other else { + return true + } + + self.directInterface = interface + return false + } + } + + nw_path_monitor_start(monitor) + } + + @MainActor + private func stopMonitoringNetworkInterfaces() { + bMonitor.cancel() + nw_path_monitor_cancel(monitor) + } + + // MARK: - Ignoring DNS flows + + private func isDnsServer(_ endpoint: NWHostEndpoint) -> Bool { + Int(endpoint.port) == Self.dnsPort + } + + // MARK: - VPN exclusions logic + + private enum FlowPath { + case block(dueTo: Reason) + case excludeFromVPN(dueTo: Reason) + case routeThroughVPN + + enum Reason { + case appRule + case domainRule + } + } + + private func path(for flow: NEAppProxyFlow) -> FlowPath { + let appIdentifier = flow.metaData.sourceAppSigningIdentifier + + switch settings.appRoutingRules[appIdentifier] { + case .none: + if let hostname = flow.remoteHostname, + isExcludedDomain(hostname) { + return .excludeFromVPN(dueTo: .domainRule) + } + + return .routeThroughVPN + case .block: + return .block(dueTo: .appRule) + case .exclude: + return .excludeFromVPN(dueTo: .domainRule) + } + } + + private func isExcludedDomain(_ hostname: String) -> Bool { + settings.excludedDomains.contains { excludedDomain in + hostname.hasSuffix(excludedDomain) + } + } + + // MARK: - Communication with App + + override public func handleAppMessage(_ messageData: Data) async -> Data? { + await appMessageHandler.handle(messageData) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift new file mode 100644 index 0000000000..3e841faeca --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/TransparentProxyProviderConfiguration.swift @@ -0,0 +1,40 @@ +// +// TransparentProxyProviderConfiguration.swift +// +// 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 + +extension TransparentProxyProvider { + /// Configuration to define behaviour for the provider based on the parent process' + /// business domain. + /// + /// This should not be passed in the startup options dictionary. + /// + public struct Configuration { + /// Whether the proxy settings should be loaded from the provider configuration in the startup options. + /// + /// We recommend setting this to true if the provider is running in a System Extension and can't access + /// shared `TransparentProxySettings`. If the provider is in an App Extension you should instead + /// use a shared `TransparentProxySettings` and set this to false. + /// + let loadSettingsFromProviderConfiguration: Bool + + public init(loadSettingsFromProviderConfiguration: Bool) { + self.loadSettingsFromProviderConfiguration = loadSettingsFromProviderConfiguration + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index 66f9bb15ea..2575803866 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -54,9 +54,11 @@ public struct NetworkProtectionStatusView: View { PromptActionView(model: promptActionViewModel) .padding(.horizontal, 5) .padding(.top, 5) + .transition(.slide) } else { if let healthWarning = model.issueDescription { connectionHealthWarningView(message: healthWarning) + .transition(.slide) } } @@ -67,12 +69,14 @@ public struct NetworkProtectionStatusView: View { if model.showDebugInformation { DebugInformationView(model: DebugInformationViewModel()) + .transition(.slide) } bottomMenuView() } .padding(5) .frame(maxWidth: 350, alignment: .top) + .transition(.slide) } // MARK: - Composite Views diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 2a86e999d0..754ca81034 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -143,9 +143,9 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in - self?.onboardingStatus = status - } - .store(in: &cancellables) + self?.onboardingStatus = status + } + .store(in: &cancellables) } func refreshLoginItemStatus() { @@ -184,14 +184,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.tunnelErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastTunnelErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastTunnelErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToControllerErrorMessages() { @@ -199,14 +199,14 @@ extension NetworkProtectionStatusView { .subscribe(on: Self.controllerErrorDispatchQueue) .sink { [weak self] errorMessage in - guard let self else { - return - } + guard let self else { + return + } - Task { @MainActor in - self.lastControllerErrorMessage = errorMessage - } - }.store(in: &cancellables) + Task { @MainActor in + self.lastControllerErrorMessage = errorMessage + } + }.store(in: &cancellables) } private func subscribeToDebugInformationChanges() { diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift new file mode 100644 index 0000000000..bd21fe50d1 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift @@ -0,0 +1,120 @@ +// +// TransparentProxyControllerPixelTests.swift +// +// 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 +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyController.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.Event, rhs: NetworkProtectionProxy.TransparentProxyController.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +extension TransparentProxyController.StartError: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyController.StartError, rhs: NetworkProtectionProxy.TransparentProxyController.StartError) -> Bool { + + let lhs = lhs as NSError + let rhs = rhs as NSError + + return lhs.code == rhs.code && lhs.domain == rhs.domain + } + + public func hash(into hasher: inout Hasher) { + (self as NSError).hash(into: &hasher) + (underlyingError as? NSError)?.hash(into: &hasher) + } +} + +final class TransparentProxyControllerPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_controller_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_controller_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_controller_start_success" + + enum TestError: PixelKitEventErrorDetails { + case testError + + static let underlyingError = NSError(domain: "test", code: 1) + + var underlyingError: Error? { + Self.underlyingError + } + } + + // MARK: - Test Firing Pixels + + func testFiringPixelsWithoutParameters() { + let tests: [TransparentProxyController.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } + + func testFiringStartFailures() { + // Just a convenience method to return the right expectation for each error + func expectaton(forError error: TransparentProxyController.StartError) -> PixelFireExpectations { + switch error { + case .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration: + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error) + case .failedToLoadConfiguration(let underlyingError), + .failedToSaveConfiguration(let underlyingError), + .failedToStartProvider(let underlyingError): + return PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: error, + underlyingError: underlyingError) + } + } + + let errors: [TransparentProxyController.StartError] = [ + .attemptToStartWithoutBackingActiveFeatures, + .couldNotEncodeSettingsSnapshot, + .couldNotRetrieveProtocolConfiguration, + .failedToLoadConfiguration(TestError.underlyingError), + .failedToSaveConfiguration(TestError.underlyingError), + .failedToStartProvider(TestError.underlyingError) + ] + + for error in errors { + verifyThat(TransparentProxyController.Event.startFailure(error), + meets: expectaton(forError: error), + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift new file mode 100644 index 0000000000..faf31729a4 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyProviderPixelTests.swift @@ -0,0 +1,66 @@ +// +// TransparentProxyProviderPixelTests.swift +// +// 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 +@testable import NetworkProtectionProxy +import PixelKit +import PixelKitTestingUtilities +import XCTest + +extension TransparentProxyProvider.Event: Hashable { + public static func == (lhs: NetworkProtectionProxy.TransparentProxyProvider.Event, rhs: NetworkProtectionProxy.TransparentProxyProvider.Event) -> Bool { + + lhs.name == rhs.name && lhs.parameters == rhs.parameters + } + + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) + parameters.hash(into: &hasher) + } +} + +final class TransparentProxyProviderPixelTests: XCTestCase { + + static let startFailureFullPixelName = "m_mac_vpn_proxy_provider_start_failure" + static let startInitiatedFullPixelName = "m_mac_vpn_proxy_provider_start_initiated" + static let startSuccessFullPixelName = "m_mac_vpn_proxy_provider_start_success" + + enum TestError: Error { + case testError + } + + // MARK: - Test Firing Pixels + + func testFiringPixels() { + let tests: [TransparentProxyProvider.Event: PixelFireExpectations] = [ + .startInitiated: PixelFireExpectations(pixelName: Self.startInitiatedFullPixelName), + .startFailure(TestError.testError): + PixelFireExpectations( + pixelName: Self.startFailureFullPixelName, + error: TestError.testError), + .startSuccess: PixelFireExpectations(pixelName: Self.startSuccessFullPixelName) + ] + + for (event, expectations) in tests { + verifyThat(event, + meets: expectations, + file: #filePath, + line: #line) + } + } +} diff --git a/LocalPackages/PixelKit/Package.swift b/LocalPackages/PixelKit/Package.swift index 61de75cd32..0cc6651ae2 100644 --- a/LocalPackages/PixelKit/Package.swift +++ b/LocalPackages/PixelKit/Package.swift @@ -20,7 +20,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index ffd9d88796..7a25f133ff 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -94,11 +94,13 @@ public extension Error { let nsError = self as NSError params[PixelKit.Parameters.errorCode] = "\(nsError.code)" - params[PixelKit.Parameters.errorDesc] = nsError.domain + params[PixelKit.Parameters.errorDomain] = nsError.domain + params[PixelKit.Parameters.errorDesc] = nsError.localizedDescription if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain + params[PixelKit.Parameters.underlyingErrorDesc] = underlyingError.localizedDescription } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index b13aea7b17..9c616d0060 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -275,11 +275,23 @@ public final class PixelKit { newParams = nil } + let newError: Error? + + if let event = event as? PixelKitEventV2 { + // For v2 events we only consider the error specified in the event + // and purposedly ignore the parameter in this call. + // This is to encourage moving the error over to the protocol error + // instead of still relying on the parameter of this call. + newError = event.error + } else { + newError = error + } + fire(pixelNamed: pixelName, frequency: frequency, withHeaders: headers, withAdditionalParameters: newParams, - withError: error, + withError: newError, allowedQueryReservedCharacters: allowedQueryReservedCharacters, includeAppVersionParameter: includeAppVersionParameter, onComplete: onComplete) @@ -365,8 +377,16 @@ extension Dictionary where Key == String, Value == String { self[PixelKit.Parameters.errorCode] = "\(nsError.code)" self[PixelKit.Parameters.errorDomain] = nsError.domain + self[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let error = error as? PixelKitEventErrorDetails, + let underlyingError = error.underlyingError { - if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { + let underlyingNSError = underlyingError as NSError + self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + self[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + self[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } else if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" self[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain } else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift index 83965ba999..bc87070df3 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEvent.swift @@ -34,7 +34,7 @@ public final class DebugEvent: PixelKitEvent { } public let eventType: EventType - private let error: Error? + public let error: Error? public init(eventType: EventType, error: Error? = nil) { self.eventType = eventType diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift new file mode 100644 index 0000000000..7048519e32 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift @@ -0,0 +1,70 @@ +// +// PixelKitEventV2.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 protocol PixelKitEventErrorDetails: Error { + var underlyingError: Error? { get } +} + +extension PixelKitEventErrorDetails { + var underlyingErrorParameters: [String: String] { + guard let nsError = underlyingError as? NSError else { + return [:] + } + + return [ + PixelKit.Parameters.underlyingErrorCode: "\(nsError.code)", + PixelKit.Parameters.underlyingErrorDomain: nsError.domain, + PixelKit.Parameters.underlyingErrorDesc: nsError.localizedDescription + ] + } +} + +/// New version of this protocol that allows us to maintain backwards-compatibility with PixelKitEvent +/// +/// This new implementation seeks to unify the handling of standard pixel parameters inside PixelKit. +/// The starting example of how this can be useful is error parameter handling - this protocol allows +/// the implementer to speciy an error without having to know about the parametrization of the error. +/// +/// The reason this wasn't done directly in `PixelKitEvent` is to reduce the risk of breaking existing +/// pixels, and to allow us to migrate towards this incrementally. +/// +public protocol PixelKitEventV2: PixelKitEvent { + var error: Error? { get } +} + +extension PixelKitEventV2 { + var pixelParameters: [String: String] { + guard let error else { + return [:] + } + + let nsError = error as NSError + var parameters = [ + PixelKit.Parameters.errorCode: "\(nsError.code)", + PixelKit.Parameters.errorDomain: nsError.domain, + ] + + if let error = error as? PixelKitEventErrorDetails { + parameters.merge(error.underlyingErrorParameters, uniquingKeysWith: { $1 }) + } + + return parameters + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift new file mode 100644 index 0000000000..067eee091e --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift @@ -0,0 +1,36 @@ +// +// PixelFireExpectations.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 + +/// Structure containing information about a pixel fire event. +/// +/// This is useful for test validation for libraries that rely on PixelKit, to make sure the pixels contain +/// all of the fields they are supposed to contain.. +/// +public struct PixelFireExpectations { + let pixelName: String + var error: Error? + var underlyingError: Error? + + public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil) { + self.pixelName = pixelName + self.error = error + self.underlyingError = underlyingError + } +} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift new file mode 100644 index 0000000000..5088ba1371 --- /dev/null +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/XCTestCase+PixelKit.swift @@ -0,0 +1,148 @@ +// +// XCTestCase+PixelKit.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 +@testable import PixelKit +import XCTest + +public extension XCTestCase { + + // MARK: - Parameters + + /// List of standard pixel parameters. + /// + /// This is useful to support filtering these parameters out if needed. + /// + private static var standardPixelParameters = [ + PixelKit.Parameters.appVersion, + PixelKit.Parameters.pixelSource, + PixelKit.Parameters.test + ] + + /// List of errror pixel parameters + /// + private static var errorPixelParameters = [ + PixelKit.Parameters.errorCode, + PixelKit.Parameters.errorDomain, + PixelKit.Parameters.errorDesc + ] + + /// List of underlying error pixel parameters + /// + private static var underlyingErrorPixelParameters = [ + PixelKit.Parameters.underlyingErrorCode, + PixelKit.Parameters.underlyingErrorDomain, + PixelKit.Parameters.underlyingErrorDesc + ] + + /// Filter out the standard parameters. + /// + private static func filterStandardPixelParameters(from parameters: [String: String]) -> [String: String] { + parameters.filter { element in + !standardPixelParameters.contains(element.key) + } + } + + static var pixelPlatformPrefix: String { +#if os(macOS) + return "m_mac_" +#else + // Intentionally left blank for now because PixelKit currently doesn't support + // other platforms, but if we decide to implement another platform this'll fail + // and indicate that we need a value here. +#endif + } + + func expectedParameters(for event: PixelKitEventV2) -> [String: String] { + var expectedParameters = [String: String]() + + if let error = event.error { + let nsError = error as NSError + expectedParameters[PixelKit.Parameters.errorCode] = "\(nsError.code)" + expectedParameters[PixelKit.Parameters.errorDomain] = nsError.domain + expectedParameters[PixelKit.Parameters.errorDesc] = nsError.localizedDescription + + if let underlyingError = (error as? PixelKitEventErrorDetails)?.underlyingError { + let underlyingNSError = underlyingError as NSError + expectedParameters[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" + expectedParameters[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain + expectedParameters[PixelKit.Parameters.underlyingErrorDesc] = underlyingNSError.localizedDescription + } + } + + return expectedParameters + } + + // MARK: - Misc Convenience + + private var userDefaults: UserDefaults { + UserDefaults(suiteName: "testing_\(UUID().uuidString)")! + } + + // MARK: - Pixel Firing Expectations + + /// Provides some snapshot of a fired pixel so that external libraries can validate all the expected info is included. + /// + /// This method also checks that there is internal consistency in the expected fields. + /// + func verifyThat(_ event: PixelKitEventV2, meets expectations: PixelFireExpectations, file: StaticString, line: UInt) { + + let expectedPixelName = Self.pixelPlatformPrefix + event.name + let expectedParameters = expectedParameters(for: event) + let callbackExecutedExpectation = expectation(description: "The PixelKit callback has been executed") + + PixelKit.setUp(dryRun: false, + appVersion: "1.0.5", + source: "test-app", + defaultHeaders: [:], + log: .disabled, + defaults: userDefaults) { firedPixelName, _, firedParameters, _, _, completion in + callbackExecutedExpectation.fulfill() + + let firedParameters = Self.filterStandardPixelParameters(from: firedParameters) + + // Internal validations + + XCTAssertEqual(firedPixelName, expectedPixelName, file: file, line: line) + XCTAssertEqual(firedParameters, expectedParameters, file: file, line: line) + + // Expectations + + XCTAssertEqual(firedPixelName, expectations.pixelName) + + if let error = expectations.error { + let nsError = error as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.errorDesc], nsError.localizedDescription, file: file, line: line) + } + + if let underlyingError = expectations.underlyingError { + let nsError = underlyingError as NSError + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorCode], String(nsError.code), file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDomain], nsError.domain, file: file, line: line) + XCTAssertEqual(firedParameters[PixelKit.Parameters.underlyingErrorDesc], nsError.localizedDescription, file: file, line: line) + } + + completion(true, nil) + } + + PixelKit.fire(event) + waitForExpectations(timeout: 0.1) + } +} diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 197a2c4eee..b7af9e0bbb 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index 15b6c3ba93..22ab992e79 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -173,14 +173,18 @@ public final class PreferencesSubscriptionModel: ObservableObject { func fetchAndUpdateSubscriptionDetails() { guard fetchSubscriptionDetailsTask == nil else { return } - fetchSubscriptionDetailsTask = Task { - guard let token = accountManager.accessToken else { return } + fetchSubscriptionDetailsTask = Task { [weak self] in + defer { + self?.fetchSubscriptionDetailsTask = nil + } + + guard let token = self?.accountManager.accessToken else { return } if let cachedDate = SubscriptionService.cachedSubscriptionDetailsResponse?.expiresOrRenewsAt { - updateDescription(for: cachedDate) + self?.updateDescription(for: cachedDate) - if cachedDate.timeIntervalSinceNow > 0 { - self.cachedEntitlements = [] + if cachedDate.timeIntervalSinceNow < 0 { + self?.cachedEntitlements = [] } } @@ -190,13 +194,13 @@ public final class PreferencesSubscriptionModel: ObservableObject { return } - updateDescription(for: response.expiresOrRenewsAt) + self?.updateDescription(for: response.expiresOrRenewsAt) - subscriptionPlatform = response.platform + self?.subscriptionPlatform = response.platform } if case let .success(entitlements) = await AccountManager().fetchEntitlements() { - self.cachedEntitlements = entitlements + self?.cachedEntitlements = entitlements } } } diff --git a/LocalPackages/SwiftUIExtensions/Package.swift b/LocalPackages/SwiftUIExtensions/Package.swift index 6f8c716bc3..249d2a6cbb 100644 --- a/LocalPackages/SwiftUIExtensions/Package.swift +++ b/LocalPackages/SwiftUIExtensions/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "PreferencesViews", targets: ["PreferencesViews"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 678077f716..8928521a9e 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../SwiftUIExtensions"), - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( @@ -23,6 +23,9 @@ let package = Package( .product(name: "PreferencesViews", package: "SwiftUIExtensions"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions") ], + resources: [ + .process("Resources") + ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) ], diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings similarity index 99% rename from LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings rename to LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings index abcf797258..340db4147a 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/Localizable.xcstrings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings @@ -209,7 +209,7 @@ } }, "paste-from-clipboard" : { - "comment" : "Paste button", + "comment" : "Paste from Clipboard button", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -245,7 +245,7 @@ } }, "preferences.begin-sync.card-footer" : { - "comment" : "Footer / captoin on the Begin Syncing card in sync settings", + "comment" : "Footer / caption on the Begin Syncing card in sync settings", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -353,7 +353,7 @@ } }, "preferences.preparing-to-sync.dialog-title" : { - "comment" : "Peparing to sync dialog title during sync set up", + "comment" : "Preparing to sync dialog title during sync set up", "extractionState" : "extracted_with_value", "localizations" : { "en" : { @@ -389,7 +389,7 @@ } }, "preferences.recover-synced-data.dialog-subtitle" : { - "comment" : "Recover synced data during Sync revoery process dialog subtitle", + "comment" : "Recover synced data during Sync recovery process dialog subtitle", "extractionState" : "extracted_with_value", "localizations" : { "en" : { diff --git a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift index ca0babbe0f..3d9356af3e 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift @@ -21,150 +21,151 @@ import Foundation enum UserText { // Generic Buttons - static let ok = NSLocalizedString("ok", value: "OK", comment: "OK button") - static let notNow = NSLocalizedString("notnow", value: "Not Now", comment: "Not Now button") - static let cancel = NSLocalizedString("cancel", value: "Cancel", comment: "Cancel button") - static let submit = NSLocalizedString("submit", value: "Submit", comment: "Submit button") - static let next = NSLocalizedString("next", value: "Next", comment: "Next button") - static let copy = NSLocalizedString("copy", value: "Copy", comment: "Copy button") - static let share = NSLocalizedString("share", value: "Share", comment: "Share button") - static let paste = NSLocalizedString("paste", value: "Paste", comment: "Paste button") - static let pasteFromClipboard = NSLocalizedString("paste-from-clipboard", value: "Paste from Clipboard", comment: "Paste button") - static let done = NSLocalizedString("done", value: "Done", comment: "Done button") + static let ok = NSLocalizedString("ok", bundle: Bundle.module, value: "OK", comment: "OK button") + static let notNow = NSLocalizedString("notnow", bundle: Bundle.module, value: "Not Now", comment: "Not Now button") + static let cancel = NSLocalizedString("cancel", bundle: Bundle.module, value: "Cancel", comment: "Cancel button") + static let submit = NSLocalizedString("submit", bundle: Bundle.module, value: "Submit", comment: "Submit button") + static let next = NSLocalizedString("next", bundle: Bundle.module, value: "Next", comment: "Next button") + static let copy = NSLocalizedString("copy", bundle: Bundle.module, value: "Copy", comment: "Copy button") + static let share = NSLocalizedString("share", bundle: Bundle.module, value: "Share", comment: "Share button") + static let paste = NSLocalizedString("paste", bundle: Bundle.module, value: "Paste", comment: "Paste button") + static let pasteFromClipboard = NSLocalizedString("paste-from-clipboard", bundle: Bundle.module, value: "Paste from Clipboard", comment: "Paste from Clipboard button") + static let done = NSLocalizedString("done", bundle: Bundle.module, value: "Done", comment: "Done button") // Sync Set Up View // Begin Sync card - static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") - static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") - static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") - static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / captoin on the Begin Syncing card in sync settings") + static let beginSyncTitle = NSLocalizedString("preferences.begin-sync.card-title", bundle: Bundle.module, value: "Begin Syncing", comment: "Begin Syncing card title in sync settings") + static let beginSyncDescription = NSLocalizedString("preferences.begin-sync.card-description", bundle: Bundle.module, value: "Securely sync bookmarks and passwords between your devices.", comment: "Begin Syncing card description in sync settings") + static let beginSyncButton = NSLocalizedString("preferences.begin-sync.card-button", bundle: Bundle.module, value: "Sync With Another Device", comment: "Button text on the Begin Syncing card in sync settings") + static let beginSyncFooter = NSLocalizedString("preferences.begin-sync.card-footer", bundle: Bundle.module, value: "Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key.", comment: "Footer / caption on the Begin Syncing card in sync settings") + // Options - static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", value: "Other Options", comment: "Sync settings. Other Options section title") - static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") - static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") + static let otherOptionsSectionTitle = NSLocalizedString("preferences.other-options.section-title", bundle: Bundle.module, value: "Other Options", comment: "Sync settings. Other Options section title") + static let syncThisDeviceLink = NSLocalizedString("preferences.sync-this-device.link-title", bundle: Bundle.module, value: "Sync and Back Up This Device", comment: "Sync settings. Title of a link to start setting up sync and backup the device") + static let recoverDataLink = NSLocalizedString("preferences.recover-data.link-title", bundle: Bundle.module, value: "Recover Synced Data", comment: "Sync settings. Link to recover synced data.") // Preparing to sync dialog - static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", value: "Preparing To Sync", comment: "Peparing to sync dialog title during sync set up") - static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") - static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", value: "Connecting…", comment: "Sync preparing to sync dialog action") + static let preparingToSyncDialogTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-title", bundle: Bundle.module, value: "Preparing To Sync", comment: "Preparing to sync dialog title during sync set up") + static let preparingToSyncDialogSubTitle = NSLocalizedString("preferences.preparing-to-sync.dialog-subtitle", bundle: Bundle.module, value: "We're setting up the connection to synchronize your bookmarks and saved logins with the other device.", comment: "Preparing to sync dialog subtitle during sync set up") + static let preparingToSyncDialogAction = NSLocalizedString("preferences.preparing-to-sync.dialog-action", bundle: Bundle.module, value: "Connecting…", comment: "Sync preparing to sync dialog action") // Enter recovery code dialog - static let enterRecoveryCodeDialogTitle = NSLocalizedString("preferences.enter-recovery-code.dialog-title", value: "Enter Code", comment: "Sync enter recovery code dialog title") - static let enterRecoveryCodeDialogSubtitle = NSLocalizedString("preferences.enter-recovery-code.dialog-subtitle", value: "Enter the code on your Recovery PDF, or another synced device, to recover your synced data.", comment: "Sync enter recovery code dialog subtitle") - static let enterRecoveryCodeDialogAction1 = NSLocalizedString("preferences.enter-recovery-code.dialog-action1", value: "Paste Code Here", comment: "Sync enter recovery code dialog first possible action") - static let enterRecoveryCodeDialogAction2 = NSLocalizedString("preferences.enter-recovery-code.dialog-action2", value: "or scan QR code with a device that is still connected", comment: "Sync enter recovery code dialog second possible action") + static let enterRecoveryCodeDialogTitle = NSLocalizedString("preferences.enter-recovery-code.dialog-title", bundle: Bundle.module, value: "Enter Code", comment: "Sync enter recovery code dialog title") + static let enterRecoveryCodeDialogSubtitle = NSLocalizedString("preferences.enter-recovery-code.dialog-subtitle", bundle: Bundle.module, value: "Enter the code on your Recovery PDF, or another synced device, to recover your synced data.", comment: "Sync enter recovery code dialog subtitle") + static let enterRecoveryCodeDialogAction1 = NSLocalizedString("preferences.enter-recovery-code.dialog-action1", bundle: Bundle.module, value: "Paste Code Here", comment: "Sync enter recovery code dialog first possible action") + static let enterRecoveryCodeDialogAction2 = NSLocalizedString("preferences.enter-recovery-code.dialog-action2", bundle: Bundle.module, value: "or scan QR code with a device that is still connected", comment: "Sync enter recovery code dialog second possible action") // Recover synced data dialog - static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", value: "Recover Synced Data", comment: "Sync recover synced data dialog title") - static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync revoery process dialog subtitle") - static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", value: "Get Started", comment: "Sync recover synced data dialog button") + static let reciverSyncedDataDialogTitle = NSLocalizedString("preferences.recover-synced-data.dialog-title", bundle: Bundle.module, value: "Recover Synced Data", comment: "Sync recover synced data dialog title") + static let reciverSyncedDataDialogSubitle = NSLocalizedString("preferences.recover-synced-data.dialog-subtitle", bundle: Bundle.module, value: "To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync.", comment: "Recover synced data during Sync recovery process dialog subtitle") + static let reciverSyncedDataDialogButton = NSLocalizedString("preferences.recover-synced-data.dialog-button", bundle: Bundle.module, value: "Get Started", comment: "Sync recover synced data dialog button") // Sync Title - static let sync = NSLocalizedString("preferences.sync", value: "Sync & Backup", comment: "Show sync preferences") - static let syncRollOutBannerDescription = NSLocalizedString("preferences.sync.rollout-banner.description", value: "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices.", comment: "Description of rollout banner") + static let sync = NSLocalizedString("preferences.sync", bundle: Bundle.module, value: "Sync & Backup", comment: "Show sync preferences") + static let syncRollOutBannerDescription = NSLocalizedString("preferences.sync.rollout-banner.description", bundle: Bundle.module, value: "Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices.", comment: "Description of rollout banner") - static let turnOff = NSLocalizedString("preferences.sync.turn-off", value: "Turn Off", comment: "Turn off sync confirmation dialog button title") - static let turnOffSync = NSLocalizedString("preferences.sync.turn-off.ellipsis", value: "Turn Off Sync…", comment: "Disable sync button caption") + static let turnOff = NSLocalizedString("preferences.sync.turn-off", bundle: Bundle.module, value: "Turn Off", comment: "Turn off sync confirmation dialog button title") + static let turnOffSync = NSLocalizedString("preferences.sync.turn-off.ellipsis", bundle: Bundle.module, value: "Turn Off Sync…", comment: "Disable sync button caption") // Sync Enabled View // Turn off sync dialog - static let turnOffSyncConfirmTitle = NSLocalizedString("preferences.sync.turn-off.confirm.title", value: "Turn off sync?", comment: "Turn off sync confirmation dialog title") - static let turnOffSyncConfirmMessage = NSLocalizedString("preferences.sync.turn-off.confirm.message", value: "This device will no longer be able to access your synced data.", comment: "Turn off sync confirmation dialog message") + static let turnOffSyncConfirmTitle = NSLocalizedString("preferences.sync.turn-off.confirm.title", bundle: Bundle.module, value: "Turn off sync?", comment: "Turn off sync confirmation dialog title") + static let turnOffSyncConfirmMessage = NSLocalizedString("preferences.sync.turn-off.confirm.message", bundle: Bundle.module, value: "This device will no longer be able to access your synced data.", comment: "Turn off sync confirmation dialog message") // Delete server data - static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") + static let turnOffAndDeleteServerData = NSLocalizedString("preferences.sync.turn-off-and-delete-data", bundle: Bundle.module, value: "Turn Off and Delete Server Data…", comment: "Disable and delete data sync button caption") // sync connected - static let syncConnected = NSLocalizedString("preferences.sync.connected", value: "Sync Enabled", comment: "Sync state is enabled") + static let syncConnected = NSLocalizedString("preferences.sync.connected", bundle: Bundle.module, value: "Sync Enabled", comment: "Sync state is enabled") // synced devices - static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", value: "Synced Devices", comment: "Settings section title") - static let thisDevice = NSLocalizedString("preferences.sync.this-device", value: "This Device", comment: "Indicator of a current user's device on the list") - static let currentDeviceDetails = NSLocalizedString("preferences.sync.current-device-details", value: "Details...", comment: "Sync Settings device details button") - static let removeDeviceButton = NSLocalizedString("preferences.sync.remove-device", value: "Remove...", comment: "Button to remove a device") + static let syncedDevices = NSLocalizedString("preferences.sync.synced-devices", bundle: Bundle.module, value: "Synced Devices", comment: "Settings section title") + static let thisDevice = NSLocalizedString("preferences.sync.this-device", bundle: Bundle.module, value: "This Device", comment: "Indicator of a current user's device on the list") + static let currentDeviceDetails = NSLocalizedString("preferences.sync.current-device-details", bundle: Bundle.module, value: "Details...", comment: "Sync Settings device details button") + static let removeDeviceButton = NSLocalizedString("preferences.sync.remove-device", bundle: Bundle.module, value: "Remove...", comment: "Button to remove a device") // Remove device dialog - static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", value: "Remove device?", comment: "Title on remove a device confirmation") - static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", value: "Remove Device", comment: "Button text on remove a device confirmation button") + static let removeDeviceConfirmTitle = NSLocalizedString("preferences.sync.remove-device-title", bundle: Bundle.module, value: "Remove device?", comment: "Title on remove a device confirmation") + static let removeDeviceConfirmButton = NSLocalizedString("preferences.sync.remove-device-button", bundle: Bundle.module, value: "Remove Device", comment: "Button text on remove a device confirmation button") static func removeDeviceConfirmMessage(_ deviceName: String) -> String { let localized = NSLocalizedString("preferences.sync.remove-device-message", - value: "\"%@\" will no longer be able to access your synced data.", + bundle: Bundle.module, value: "\"%@\" will no longer be able to access your synced data.", comment: "Message to confirm the device will no longer be able to access the synced data - devoce name item inserted") return String(format: localized, deviceName) } - static let recovery = NSLocalizedString("prefrences.sync.recovery", value: "Recovery", comment: "Sync settings section title") - static let recoveryInstructions = NSLocalizedString("prefrences.sync.recovery-instructions", value: "If you lose your device, you will need this recovery code to restore your synced data.", comment: "Instructions on how to restore synced data") + static let recovery = NSLocalizedString("prefrences.sync.recovery", bundle: Bundle.module, value: "Recovery", comment: "Sync settings section title") + static let recoveryInstructions = NSLocalizedString("prefrences.sync.recovery-instructions", bundle: Bundle.module, value: "If you lose your device, you will need this recovery code to restore your synced data.", comment: "Instructions on how to restore synced data") // Sync with another device dialog - static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", value: "Sync With Another Device", comment: "Sync with another device dialog title") + static let syncWithAnotherDeviceTitle = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-title", bundle: Bundle.module, value: "Sync With Another Device", comment: "Sync with another device dialog title") static func syncWithAnotherDeviceSubtitle(syncMenuPath: String) -> String { - let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") + let localized = NSLocalizedString("preferences.sync.sync-with-another-device.dialog-subtitle1", bundle: Bundle.module, value: "Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device.", comment: "Sync with another device dialog subtitle - Instruction with sync menu path item inserted") return String(format: localized, syncMenuPath) } - static let syncMenuPath = NSLocalizedString("sync.menu.path", value: "Settings › Sync & Backup", comment: "Sync Menu Path") - static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", value: "Show Code", comment: "Text on show code button on Sync with another device dialog") - static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") - static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") - static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") - static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") - static let syncWithAnotherDeviceViewQRCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-qr-code-link", value: "View QR Code", comment: "Sync with another device dialog view qr code link") - static let syncWithAnotherDeviceViewTextCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-text-code-link", value: "View Text Code", comment: "Sync with another device dialog view text code link") + static let syncMenuPath = NSLocalizedString("sync.menu.path", bundle: Bundle.module, value: "Settings › Sync & Backup", comment: "Sync Menu Path") + static let syncWithAnotherDeviceShowCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-button", bundle: Bundle.module, value: "Show Code", comment: "Text on show code button on Sync with another device dialog") + static let syncWithAnotherDeviceEnterCodeButton = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-button", bundle: Bundle.module, value: "Enter Code", comment: "Text on enter code button on Sync with another device dialog") + static let syncWithAnotherDeviceShowQRCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-qr-code-explanation", bundle: Bundle.module, value: "Scan this QR code to connect.", comment: "Sync with another device dialog show qr code explanation") + static let syncWithAnotherDeviceEnterCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.enter-code-explanation", bundle: Bundle.module, value: "Paste the code here to sync.", comment: "Sync with another device dialog enter code explanation") + static let syncWithAnotherDeviceShowCodeExplanation = NSLocalizedString("preferences.sync.sync-with-another-device.show-code-explanation", bundle: Bundle.module, value: "Share this code to connect with a desktop machine.", comment: "Sync with another device dialog show code explanation") + static let syncWithAnotherDeviceViewQRCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-qr-code-link", bundle: Bundle.module, value: "View QR Code", comment: "Sync with another device dialog view qr code link") + static let syncWithAnotherDeviceViewTextCode = NSLocalizedString("preferences.sync.sync-with-another-device.view-text-code-link", bundle: Bundle.module, value: "View Text Code", comment: "Sync with another device dialog view text code link") // Save recovery PDF dialog - static let saveRecoveryPDF = NSLocalizedString("prefrences.sync.save-recovery-pdf", value: "Save Your Recovery Code", comment: "Caption for a button to save Sync recovery PDF") - static let recoveryPDFExplanation = NSLocalizedString("prefrences.sync.recovery-pdf-explanation", value: "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF.", comment: "Sync recovery PDF explanation") - static let recoveryPDFCopyCodeButton = NSLocalizedString("prefrences.sync.recovery-pdf-copy-code-button", value: "Copy Code", comment: "Sync recovery PDF copy code button") - static let recoveryPDFSavePDFButton = NSLocalizedString("prefrences.sync.recovery-pdf-save-pdf-button", value: "Save PDF", comment: "Sync recovery PDF save pdf button") - static let recoveryPDFWarning = NSLocalizedString("prefrences.sync.recovery-pdf-warning", value: "Anyone with access to this code can access your synced data, so please keep it in a safe place.", comment: "Sync recovery PDF warning") + static let saveRecoveryPDF = NSLocalizedString("prefrences.sync.save-recovery-pdf", bundle: Bundle.module, value: "Save Your Recovery Code", comment: "Caption for a button to save Sync recovery PDF") + static let recoveryPDFExplanation = NSLocalizedString("prefrences.sync.recovery-pdf-explanation", bundle: Bundle.module, value: "If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF.", comment: "Sync recovery PDF explanation") + static let recoveryPDFCopyCodeButton = NSLocalizedString("prefrences.sync.recovery-pdf-copy-code-button", bundle: Bundle.module, value: "Copy Code", comment: "Sync recovery PDF copy code button") + static let recoveryPDFSavePDFButton = NSLocalizedString("prefrences.sync.recovery-pdf-save-pdf-button", bundle: Bundle.module, value: "Save PDF", comment: "Sync recovery PDF save pdf button") + static let recoveryPDFWarning = NSLocalizedString("prefrences.sync.recovery-pdf-warning", bundle: Bundle.module, value: "Anyone with access to this code can access your synced data, so please keep it in a safe place.", comment: "Sync recovery PDF warning") // Sync with server dialog - static let syncWithServerTitle = NSLocalizedString("preferences.sync.sync-with-server-title", value: "Sync and Back Up This Device", comment: "Sync with server dialog title") - static let syncWithServerSubtitle1 = NSLocalizedString("preferences.sync.sync-with-server-subtitle1", value: "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.", comment: "Sync with server dialog first subtitle") - static let syncWithServerSubtitle2 = NSLocalizedString("preferences.sync.sync-with-server-subtitle2", value: "The encryption key is only stored on your device, DuckDuckGo cannot access it.", comment: "Sync with server dialog second subtitle") - static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", value: "Turn On Sync & Backup", comment: "Sync with server dialog button") + static let syncWithServerTitle = NSLocalizedString("preferences.sync.sync-with-server-title", bundle: Bundle.module, value: "Sync and Back Up This Device", comment: "Sync with server dialog title") + static let syncWithServerSubtitle1 = NSLocalizedString("preferences.sync.sync-with-server-subtitle1", bundle: Bundle.module, value: "This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices.", comment: "Sync with server dialog first subtitle") + static let syncWithServerSubtitle2 = NSLocalizedString("preferences.sync.sync-with-server-subtitle2", bundle: Bundle.module, value: "The encryption key is only stored on your device, DuckDuckGo cannot access it.", comment: "Sync with server dialog second subtitle") + static let syncWithServerButton = NSLocalizedString("preferences.sync.sync-with-server-button", bundle: Bundle.module, value: "Turn On Sync & Backup", comment: "Sync with server dialog button") // Device synced dialog - static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", value: "Your data is synced!", comment: "Sync setup confirmation dialog title") + static let deviceSynced = NSLocalizedString("prefrences.sync.device-synced", bundle: Bundle.module, value: "Your data is synced!", comment: "Sync setup confirmation dialog title") // Device details - static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", value: "Device Details", comment: "The title of the device details dialog") - static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", value: "Name", comment: "The text entry label to name the device") - static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", value: "Device name", comment: "The text entry prompt to name the device") + static let deviceDetailsTitle = NSLocalizedString("prefrences.sync.device-details.title", bundle: Bundle.module, value: "Device Details", comment: "The title of the device details dialog") + static let deviceDetailsLabel = NSLocalizedString("prefrences.sync.device-details.label", bundle: Bundle.module, value: "Name", comment: "The text entry label to name the device") + static let deviceDetailsPrompt = NSLocalizedString("prefrences.sync.device-details.prompt", bundle: Bundle.module, value: "Device name", comment: "The text entry prompt to name the device") // Delete Account Dialog - static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", value: "Delete server data?", comment: "Title for delete account confirmation pop up") - static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") - static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", value: "Delete Data", comment: "Label for delete account button") + static let deleteAccountTitle = NSLocalizedString("prefrences.sync.delete-account.title", bundle: Bundle.module, value: "Delete server data?", comment: "Title for delete account confirmation pop up") + static let deleteAccountMessage = NSLocalizedString("prefrences.sync.delete-account.message", bundle: Bundle.module, value: "These devices will be disconnected and your synced data will be deleted from the server.", comment: "Message for delete account confirmation pop up") + static let deleteAccountButton = NSLocalizedString("prefrences.sync.delete-account.button", bundle: Bundle.module, value: "Delete Data", comment: "Label for delete account button") // Sync enabled options - static let optionsSectionTitle = NSLocalizedString("prefrences.sync.options-section-title", value: "Options", comment: "Title for options settings") - static let shareFavoritesOptionTitle = NSLocalizedString("prefrences.sync.share-favorite-option-title", value: "Unify Favorites Across Devices", comment: "Title for share favorite option") - static let shareFavoritesOptionCaption = NSLocalizedString("prefrences.sync.share-favorite-option-caption", value: "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate.", comment: "Caption for share favorite option") - static let fetchFaviconsOptionTitle = NSLocalizedString("prefrences.sync.fetch-favicons-option-title", value: "Auto-Download Icons", comment: "Title for fetch favicons option") - static let fetchFaviconsOptionCaption = NSLocalizedString("prefrences.sync.fetch-favicons-option-caption", value: "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network.", comment: "Caption for fetch favicons option") + static let optionsSectionTitle = NSLocalizedString("prefrences.sync.options-section-title", bundle: Bundle.module, value: "Options", comment: "Title for options settings") + static let shareFavoritesOptionTitle = NSLocalizedString("prefrences.sync.share-favorite-option-title", bundle: Bundle.module, value: "Unify Favorites Across Devices", comment: "Title for share favorite option") + static let shareFavoritesOptionCaption = NSLocalizedString("prefrences.sync.share-favorite-option-caption", bundle: Bundle.module, value: "Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate.", comment: "Caption for share favorite option") + static let fetchFaviconsOptionTitle = NSLocalizedString("prefrences.sync.fetch-favicons-option-title", bundle: Bundle.module, value: "Auto-Download Icons", comment: "Title for fetch favicons option") + static let fetchFaviconsOptionCaption = NSLocalizedString("prefrences.sync.fetch-favicons-option-caption", bundle: Bundle.module, value: "Automatically download icons for synced bookmarks. Icon downloads are exposed to your network.", comment: "Caption for fetch favicons option") // sync enabled errors - static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", value: "Sync Paused", comment: "Title for sync limits exceeded warning") - static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", value: "Bookmark limit exceeded. Delete some to resume syncing.", comment: "Description for sync bookmarks limits exceeded warning") - static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Description for sync credentials limits exceeded warning") - static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks") - static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") - static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", value: "Sync & Backup Error", comment: "Title for sync error alert") - static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") - static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") - static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") - static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", value: "Unable to update the device name.", comment: "Description for unable to update device name error") - static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") - static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") - static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") - static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error") - static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") - - static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") - static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog") - static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") + static let syncLimitExceededTitle = NSLocalizedString("prefrences.sync.limit-exceeded-title", bundle: Bundle.module, value: "Sync Paused", comment: "Title for sync limits exceeded warning") + static let bookmarksLimitExceededDescription = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-description", bundle: Bundle.module, value: "Bookmark limit exceeded. Delete some to resume syncing.", comment: "Description for sync bookmarks limits exceeded warning") + static let credentialsLimitExceededDescription = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-description", bundle: Bundle.module, value: "Logins limit exceeded. Delete some to resume syncing.", comment: "Description for sync credentials limits exceeded warning") + static let bookmarksLimitExceededAction = NSLocalizedString("prefrences.sync.bookmarks-limit-exceeded-action", bundle: Bundle.module, value: "Manage Bookmarks", comment: "Button title for sync bookmarks limits exceeded warning to go to manage bookmarks") + static let credentialsLimitExceededAction = NSLocalizedString("prefrences.sync.credentials-limit-exceeded-action", bundle: Bundle.module, value: "Manage passwords…", comment: "Button title for sync credentials limits exceeded warning to go to manage passwords") + static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", bundle: Bundle.module, value: "Sync & Backup Error", comment: "Title for sync error alert") + static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", bundle: Bundle.module, value: "Unable to connect to the server.", comment: "Description for unable to sync to server error") + static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", bundle: Bundle.module, value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error") + static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", bundle: Bundle.module, value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error") + static let unableToUpdateDeviceNameDescription = NSLocalizedString("alert.unable-to-update-device-name-description", bundle: Bundle.module, value: "Unable to update the device name.", comment: "Description for unable to update device name error") + static let unableToTurnSyncOffDescription = NSLocalizedString("alert.unable-to-turn-sync-off-description", bundle: Bundle.module, value: "Unable to turn Sync & Backup off.", comment: "Description for unable to turn sync off error") + static let unableToDeleteDataDescription = NSLocalizedString("alert.unable-to-delete-data-description", bundle: Bundle.module, value: "Unable to delete data on the server.", comment: "Description for unable to delete data error") + static let unableToRemoveDeviceDescription = NSLocalizedString("alert.unable-to-remove-device-description", bundle: Bundle.module, value: "Unable to remove this device from Sync & Backup.", comment: "Description for unable to remove device error") + static let invalidCodeDescription = NSLocalizedString("alert.invalid-code-description", bundle: Bundle.module, value: "Sorry, this code is invalid. Please make sure it was entered correctly.", comment: "Description for invalid code error") + static let unableCreateRecoveryPdfDescription = NSLocalizedString("alert.unable-to-create-recovery-pdf-description", bundle: Bundle.module, value: "Unable to create the recovery PDF.", comment: "Description for unable to create recovery pdf error") + + static let fetchFaviconsOnboardingTitle = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-title", bundle: Bundle.module, value: "Download Missing Icons?", comment: "Title for fetch favicons onboarding dialog") + static let fetchFaviconsOnboardingMessage = NSLocalizedString("prefrences.sync.fetch-favicons-onboarding-message", bundle: Bundle.module, value: "Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced.", comment: "Text for fetch favicons onboarding dialog") + static let keepFaviconsUpdated = NSLocalizedString("prefrences.sync.keep-favicons-updated", bundle: Bundle.module, value: "Keep Bookmarks Icons Updated", comment: "Title of the confirmation button for favicons fetching") // Sync Feature Flags - static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") - static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") - static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") - static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") + static let syncUnavailableTitle = NSLocalizedString("sync.warning.sync-unavailable", bundle: Bundle.module, value: "Sync & Backup is Unavailable", comment: "Title of the warning message that sync and backup are unavailable") + static let syncPausedTitle = NSLocalizedString("sync.warning.sync-paused", bundle: Bundle.module, value: "Sync & Backup is Paused", comment: "Title of the warning message that Sync & Backup is Paused") + static let syncUnavailableMessage = NSLocalizedString("sync.warning.sync-unavailable-message", bundle: Bundle.module, value: "Sorry, but Sync & Backup is currently unavailable. Please try again later.", comment: "Data syncing unavailable warning message") + static let syncUnavailableMessageUpgradeRequired = NSLocalizedString("sync.warning.data-syncing-disabled-upgrade-required", bundle: Bundle.module, value: "Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue.", comment: "Data syncing unavailable warning message") } diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index 188a15c3ad..0ae14903d5 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -16,7 +16,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/XPCHelper/Package.swift b/LocalPackages/XPCHelper/Package.swift index fb49a79f25..323f247b91 100644 --- a/LocalPackages/XPCHelper/Package.swift +++ b/LocalPackages/XPCHelper/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "XPCHelper", targets: ["XPCHelper"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "109.0.2"), ], targets: [ .target( diff --git a/NetworkProtectionSystemExtension/Info.plist b/NetworkProtectionSystemExtension/Info.plist index c35c8be1ca..43844d5702 100644 --- a/NetworkProtectionSystemExtension/Info.plist +++ b/NetworkProtectionSystemExtension/Info.plist @@ -14,6 +14,8 @@ com.apple.networkextension.packet-tunnel $(PRODUCT_MODULE_NAME).MacPacketTunnelProvider + com.apple.networkextension.app-proxy + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider MAIN_BUNDLE_IDENTIFIER $(MAIN_BUNDLE_IDENTIFIER) diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements index a049fa6886..4252e67c8e 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Debug.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider + app-proxy-provider com.apple.security.app-sandbox diff --git a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements index f7d87546d2..23068f001f 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements +++ b/NetworkProtectionSystemExtension/NetworkProtectionSystemExtension_Release.entitlements @@ -5,6 +5,7 @@ com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension + app-proxy-provider-systemextension com.apple.security.app-sandbox diff --git a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift index 6e34010422..c9f063d54f 100644 --- a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift +++ b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift @@ -149,17 +149,19 @@ class PinnedTabsViewModelTests: XCTestCase { model.fireproof(tabA) model.removeFireproofing(tabB) model.close(tabA) + model.muteOrUmute(tabB) cancellable.cancel() - XCTAssertEqual(events.count, 6) + XCTAssertEqual(events.count, 7) guard case .bookmark(tabA) = events[0], case .unpin(1) = events[1], case .duplicate(0) = events[2], case .fireproof(tabA) = events[3], case .removeFireproofing(tabB) = events[4], - case .close(0) = events[5] + case .close(0) = events[5], + case .muteOrUnmute(tabB) = events[6] else { XCTFail("Incorrect context menu action") return diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 9d576f6550..ab577650a2 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -22,6 +22,7 @@ import Foundation class MockTabViewItemDelegate: TabBarViewItemDelegate { var hasItemsToTheRight = false + var audioState: WKWebView.AudioState = .notSupported func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { @@ -75,8 +76,20 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + return audioState + } + + func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) { + + } + func otherTabBarViewItemsState(for tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> DuckDuckGo_Privacy_Browser.OtherTabBarViewItemsState { OtherTabBarViewItemsState(hasItemsToTheLeft: false, hasItemsToTheRight: hasItemsToTheRight) } + func clear() { + self.audioState = .notSupported + } + } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index d855470109..c4e8f4418b 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -33,6 +33,10 @@ final class TabBarViewItemTests: XCTestCase { tabBarViewItem.delegate = delegate } + override func tearDown() { + delegate.clear() + } + func testThatAllExpectedItemsAreShown() { tabBarViewItem.menuNeedsUpdate(menu) @@ -48,6 +52,24 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) } + func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { + delegate.audioState = .unmuted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + + func testThatUnmuteIsShownWhenCurrentAudioStateIsMuted() { + delegate.audioState = .muted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu) diff --git a/VPNProxyExtension/Info.plist b/VPNProxyExtension/Info.plist new file mode 100644 index 0000000000..7f2489c298 --- /dev/null +++ b/VPNProxyExtension/Info.plist @@ -0,0 +1,17 @@ + + + + + DISTRIBUTED_NOTIFICATIONS_PREFIX + $(DISTRIBUTED_NOTIFICATIONS_PREFIX) + NETP_APP_GROUP + $(NETP_APP_GROUP) + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.app-proxy + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).MacTransparentProxyProvider + + + diff --git a/VPNProxyExtension/VPNProxyExtension.entitlements b/VPNProxyExtension/VPNProxyExtension.entitlements new file mode 100644 index 0000000000..968c758f97 --- /dev/null +++ b/VPNProxyExtension/VPNProxyExtension.entitlements @@ -0,0 +1,25 @@ + + + + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.duckduckgo.macos.browser.network-protection + $(NETP_APP_GROUP) + + com.apple.security.app-sandbox + + com.apple.security.network.server + + keychain-access-groups + + $(NETP_APP_GROUP) + + com.apple.developer.networking.networkextension + + app-proxy-provider + + com.apple.security.network.client + + + diff --git a/fastlane/Matchfile b/fastlane/Matchfile index 8dbddfccb8..2af9eed1fe 100644 --- a/fastlane/Matchfile +++ b/fastlane/Matchfile @@ -11,6 +11,8 @@ app_identifier [ "com.duckduckgo.mobile.ios.review", "com.duckduckgo.mobile.ios.vpn.agent.review", "com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension", + "com.duckduckgo.mobile.ios.vpn.agent.proxy", + "com.duckduckgo.mobile.ios.vpn.agent.review.proxy", "com.duckduckgo.mobile.ios.DBP.backgroundAgent.review", "com.duckduckgo.mobile.ios.DBP.backgroundAgent" diff --git a/scripts/assets/AppStoreExportOptions.plist b/scripts/assets/AppStoreExportOptions.plist index b9395f914a..9ebf3d7885 100644 --- a/scripts/assets/AppStoreExportOptions.plist +++ b/scripts/assets/AppStoreExportOptions.plist @@ -14,12 +14,16 @@ match AppStore com.duckduckgo.mobile.ios.vpn.agent macos com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.proxy macos com.duckduckgo.mobile.ios.review match AppStore com.duckduckgo.mobile.ios.review macos com.duckduckgo.mobile.ios.vpn.agent.review match AppStore com.duckduckgo.mobile.ios.vpn.agent.review macos com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.network-protection-extension macos + com.duckduckgo.mobile.ios.vpn.agent.review.proxy + match AppStore com.duckduckgo.mobile.ios.vpn.agent.review.proxy macos com.duckduckgo.mobile.ios.DBP.backgroundAgent match AppStore com.duckduckgo.mobile.ios.DBP.backgroundAgent macos com.duckduckgo.mobile.ios.DBP.backgroundAgent.review diff --git a/scripts/upload_to_s3/upload_to_s3.sh b/scripts/upload_to_s3/upload_to_s3.sh index ac7979f4c2..ddce4555c6 100755 --- a/scripts/upload_to_s3/upload_to_s3.sh +++ b/scripts/upload_to_s3/upload_to_s3.sh @@ -2,6 +2,7 @@ # Constants S3_PATH="s3://ddg-staticcdn/macos-desktop-browser/" +CDN_PATH="https://staticcdn.duckduckgo.com/macos-desktop-browser/" # Defaults if [[ -n "$CI" ]]; then @@ -149,13 +150,13 @@ for FILENAME in $FILES_TO_UPLOAD; do fi # Check if the file exists on S3 - AWS_CMD="$AWS s3 ls ${S3_PATH}${FILENAME}" - echo "Checking S3 for ${S3_PATH}${FILENAME}..." - if ! $AWS s3 ls "${S3_PATH}${FILENAME}" > /dev/null 2>&1; then - echo "$FILENAME not found on S3. Marking for upload." - MISSING_FILES+=("$FILENAME") + printf '%s' "Checking CDN for ${CDN_PATH}${FILENAME} ... " + if curl -fLSsI "${CDN_PATH}${FILENAME}" >/dev/null 2>&1; then + echo "✅" else - echo "$FILENAME exists on S3. Skipping." + echo "❌" + echo "🚢 Marking $FILENAME for upload." + MISSING_FILES+=("$FILENAME") fi done