Skip to content

Commit

Permalink
VPN Domain exclusions (internal release) (#3045)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1207936340790549/f

BSK PR: duckduckgo/BrowserServicesKit#918
iOS PR: duckduckgo/iOS#3164

## Description

Implements domain exclusions for internal users only.
  • Loading branch information
diegoreymendez authored Aug 5, 2024
1 parent 4ca12bf commit 879b969
Show file tree
Hide file tree
Showing 57 changed files with 2,046 additions and 263 deletions.
70 changes: 69 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/BrowserServicesKit",
"state" : {
"revision" : "92ecebfb4172ab9561959a07d7ef7037aea8c6e1",
"version" : "180.0.0"
"revision" : "a3b3df069bbaa06149e43ca26e5df219ee61aa15",
"version" : "180.0.1"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@
<Test
Identifier = "NavigationProtectionIntegrationTests/testReferrerTrimming()">
</Test>
<Test
Identifier = "TabContentTests/testWhenPDFContextMenuPrintChosen_printDialogOpens()">
</Test>
<Test
Identifier = "TabContentTests/testWhenPDFMainMenuPrintChosen_printDialogOpens()">
</Test>
</SkippedTests>
</TestableReference>
<TestableReference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@
<Test
Identifier = "NavigationProtectionIntegrationTests/testReferrerTrimming()">
</Test>
<Test
Identifier = "TabContentTests/testWhenPDFContextMenuPrintChosen_printDialogOpens()">
</Test>
<Test
Identifier = "TabContentTests/testWhenPDFContextMenuSaveAsChosen_saveDialogOpens()">
</Test>
</SkippedTests>
</TestableReference>
<TestableReference
Expand Down
39 changes: 6 additions & 33 deletions DuckDuckGo/Application/URLEventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import DataBrokerProtection
// @MainActor
final class URLEventHandler {

@MainActor
private static let vpnURLEventHandler = VPNURLEventHandler()

private let handler: (URL) -> Void

private var didFinishLaunching = false
Expand Down Expand Up @@ -109,7 +112,9 @@ final class URLEventHandler {

private static func openURL(_ url: URL) {
if url.scheme?.isNetworkProtectionScheme == true {
handleNetworkProtectionURL(url)
Task { @MainActor in
await vpnURLEventHandler.handle(url)
}
}

#if DBP
Expand Down Expand Up @@ -141,38 +146,6 @@ final class URLEventHandler {
}
}

/// Handles NetP URLs
private static func handleNetworkProtectionURL(_ url: URL) {
DispatchQueue.main.async {
switch url {
case VPNAppLaunchCommand.showStatus.launchURL:
Task {
await WindowControllersManager.shared.showNetworkProtectionStatus()
}
case VPNAppLaunchCommand.showSettings.launchURL:
WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn)
case VPNAppLaunchCommand.shareFeedback.launchURL:
WindowControllersManager.shared.showShareFeedbackModal()
case VPNAppLaunchCommand.justOpen.launchURL:
WindowControllersManager.shared.showMainWindow()
case VPNAppLaunchCommand.showVPNLocations.launchURL:
WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn)
WindowControllersManager.shared.showLocationPickerSheet()
case VPNAppLaunchCommand.showPrivacyPro.launchURL:
let url = Application.appDelegate.subscriptionManager.url(for: .purchase)
WindowControllersManager.shared.showTab(with: .subscription(url))
PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression)
#if !APPSTORE && !DEBUG
case VPNAppLaunchCommand.moveAppToApplications.launchURL:
// this should be run after NSApplication.shared is set
PFMoveToApplicationsFolderIfNecessary(false)
#endif
default:
return
}
}
}

#if DBP
/// Handles DBP URLs
///
Expand Down
12 changes: 12 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ extension UserText {
// MARK: - Setting Titles
// "vpn.location.title" - Location section title in VPN settings
static let vpnLocationTitle = "Location"
// "vpn.excluded.sites.title" - Excluded Sites title in VPN settings
static let vpnExcludedSitesTitle = "Excluded Sites"
// "vpn.general.title" - General section title in VPN settings
static let vpnGeneralTitle = "General"
// "vpn.shortcuts.settings.title" - Shortcuts section title in VPN settings
Expand Down Expand Up @@ -161,6 +163,16 @@ extension UserText {
return String(format: message, count)
}

// MARK: - Excluded Domains
// "vpn.setting.excluded.domains.description" - Excluded Sites description
static let vpnExcludedDomainsDescription = "Websites you selected to be excluded even when the VPN is connected."
// "vpn.setting.excluded.domains.manage.button.title" - Excluded Sites management button title
static let vpnExcludedDomainsManageButtonTitle = "Manage Excluded Sites…"
// "vpn.excluded.domains.add.domain" - Add Domain button for the excluded sites view
static let vpnExcludedDomainsAddDomain = "Add Website"
// "vpn.excluded.domains.title" - Title for the excluded sites view
static let vpnExcludedDomainsTitle = "Excluded Websites"

// MARK: - DNS
// "vpn.dns.server.title" - Title of the DNS Server section
static let vpnDnsServerTitle = "DNS Server"
Expand Down
3 changes: 3 additions & 0 deletions DuckDuckGo/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -57121,6 +57121,9 @@
}
}
}
},
"URL" : {

},
"version" : {
"comment" : "Displays the version and build numbers",
Expand Down
20 changes: 14 additions & 6 deletions DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ protocol PopoverPresenter {
func show(_ popover: NSPopover, positionedBelow view: NSView)
}

@MainActor
protocol NetPPopoverManager: AnyObject {
var isShown: Bool { get }

Expand Down Expand Up @@ -133,8 +134,10 @@ final class NavigationBarPopovers: NSObject, PopoverPresenter {
}

func toggleNetworkProtectionPopover(from button: MouseOverButton, withDelegate delegate: NSPopoverDelegate) {
if let popover = networkProtectionPopoverManager.toggle(positionedBelow: button, withDelegate: delegate) {
bindIsMouseDownState(of: button, to: popover)
Task { @MainActor in
if let popover = networkProtectionPopoverManager.toggle(positionedBelow: button, withDelegate: delegate) {
bindIsMouseDownState(of: button, to: popover)
}
}
}

Expand Down Expand Up @@ -199,8 +202,10 @@ final class NavigationBarPopovers: NSObject, PopoverPresenter {
downloadsPopover?.close()
}

if networkProtectionPopoverManager.isShown {
networkProtectionPopoverManager.close()
Task { @MainActor in
if networkProtectionPopoverManager.isShown {
networkProtectionPopoverManager.close()
}
}

if bookmarkPopover?.isShown ?? false {
Expand Down Expand Up @@ -432,8 +437,11 @@ final class NavigationBarPopovers: NSObject, PopoverPresenter {
// MARK: - VPN

func showNetworkProtectionPopover(positionedBelow button: MouseOverButton, withDelegate delegate: NSPopoverDelegate) {
let popover = networkProtectionPopoverManager.show(positionedBelow: button, withDelegate: delegate)
bindIsMouseDownState(of: button, to: popover)

Task { @MainActor in
let popover = networkProtectionPopoverManager.show(positionedBelow: button, withDelegate: delegate)
bindIsMouseDownState(of: button, to: popover)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// ActiveDomainPublisher.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

/// A convenience class for publishing the active domain
///
/// The active domain is the domain loaded in the last active tab within the last active window.
///
final class ActiveDomainPublisher {

private let windowControllersManager: WindowControllersManager
private var activeWindowControllerCancellable: AnyCancellable?
private var activeTabViewModelCancellable: AnyCancellable?
private var activeTabContentCancellable: AnyCancellable?

@MainActor
@Published
private var activeWindowController: MainWindowController? {
didSet {
subscribeToActiveTabViewModel()
}
}

@MainActor
@Published
private var activeTab: Tab? {
didSet {
subscribeToActiveTabContentChanges()
}
}

init(windowControllersManager: WindowControllersManager) {
self.windowControllersManager = windowControllersManager

Task { @MainActor in
subscribeToKeyWindowControllerChanges()
}
}

@Published
private(set) var activeDomain: String?

@MainActor
private func subscribeToKeyWindowControllerChanges() {
activeWindowControllerCancellable = windowControllersManager
.didChangeKeyWindowController
.prepend(windowControllersManager.lastKeyMainWindowController)
.assign(to: \.activeWindowController, onWeaklyHeld: self)
}

@MainActor
private func subscribeToActiveTabViewModel() {
activeTabViewModelCancellable = activeWindowController?.mainViewController.tabCollectionViewModel.$selectedTabViewModel
.map(\.?.tab)
.assign(to: \.activeTab, onWeaklyHeld: self)
}

@MainActor
private func subscribeToActiveTabContentChanges() {
activeTabContentCancellable = activeTab?.$content
.map(domain(from:))
.removeDuplicates()
.assign(to: \.activeDomain, onWeaklyHeld: self)
}

private func domain(from tabContent: Tab.TabContent) -> String? {
if case .url(let url, _, _) = tabContent {

return url.host
} else {
return nil
}
}
}

extension ActiveDomainPublisher: Publisher {
typealias Output = String?
typealias Failure = Never

func receive<S>(subscriber: S) where S: Subscriber, Never == S.Failure, String? == S.Input {
$activeDomain.subscribe(subscriber)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ final class NetworkProtectionDebugMenu: NSMenu {

NSMenuItem.separator()

NSMenuItem(title: "Adapter") {
NSMenuItem(title: "Restart Adapter", action: #selector(NetworkProtectionDebugMenu.restartAdapter(_:)))
.targetting(self)

NSMenuItem(title: "Re-create Adapter", action: #selector(NetworkProtectionDebugMenu.restartAdapter(_:)))
.targetting(self)
}

NSMenuItem(title: "Tunnel Settings") {
shouldIncludeAllNetworksMenuItem
.targetting(self)
Expand Down Expand Up @@ -218,6 +226,18 @@ final class NetworkProtectionDebugMenu: NSMenu {
}
}

/// Removes the system extension and agents for DuckDuckGo VPN.
///
@objc func restartAdapter(_ sender: Any?) {
Task { @MainActor in
do {
try await debugUtilities.restartAdapter()
} catch {
await NSAlert(error: error).runModal()
}
}
}

/// Sends a test user notification.
///
@objc func sendTestNotification(_ sender: Any?) {
Expand Down Expand Up @@ -449,8 +469,8 @@ final class NetworkProtectionDebugMenu: NSMenu {
private let ddgBrowserAppIdentifier = Bundle.main.bundleIdentifier!

private func updateExclusionsMenu() {
excludeDBPTrafficFromVPN.state = transparentProxySettings.isExcluding(dbpBackgroundAppIdentifier) ? .on : .off
excludeDDGBrowserTrafficFromVPN.state = transparentProxySettings.isExcluding(ddgBrowserAppIdentifier) ? .on : .off
excludeDBPTrafficFromVPN.state = transparentProxySettings.isExcluding(appIdentifier: dbpBackgroundAppIdentifier) ? .on : .off
excludeDDGBrowserTrafficFromVPN.state = transparentProxySettings.isExcluding(appIdentifier: ddgBrowserAppIdentifier) ? .on : .off
}

@objc private func toggleExcludeDBPBackgroundAgent() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ final class NetworkProtectionDebugUtilities {

// MARK: - Debug commands for the extension

func restartAdapter() async throws {
try await ipcClient.command(.restartAdapter)
}

func resetAllState(keepAuthToken: Bool) async throws {
try await vpnUninstaller.uninstall(removeSystemExtension: true)

Expand Down
Loading

0 comments on commit 879b969

Please sign in to comment.