From 8c1c490da281d28d84fb48902fef65bc49d74c3c Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Wed, 6 Mar 2019 15:30:42 +0530 Subject: [PATCH] on-demand: macOS: Support SSIDs in on demand activation --- WireGuard/WireGuard.xcodeproj/project.pbxproj | 8 ++ .../WireGuard/UI/macOS/View/ControlRow.swift | 61 ++++++++++++ .../UI/macOS/View/OnDemandWiFiControls.swift | 97 +++++++++++++++++++ .../TunnelEditViewController.swift | 51 +++++----- 4 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 WireGuard/WireGuard/UI/macOS/View/ControlRow.swift create mode 100644 WireGuard/WireGuard/UI/macOS/View/OnDemandWiFiControls.swift diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index 9716b42..954b3f7 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -77,6 +77,7 @@ 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; }; 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; }; 6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; }; + 6F86476B222FBB07006925D9 /* ControlRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F864769222FB87C006925D9 /* ControlRow.swift */; }; 6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */; }; 6F89E17C21F090CC00C97BB9 /* TunnelsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */; }; 6F8F0D7122258153000E8335 /* ActivateOnDemandViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */; }; @@ -90,6 +91,7 @@ 6F919EDC218C65C50023B400 /* wireguard_doc_logo_320x320.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */; }; 6F9B582921E8D6D100544D02 /* PopupRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9B582721E8CD4300544D02 /* PopupRow.swift */; }; 6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1017821C57DE600766195 /* MockTunnels.swift */; }; + 6FB17946222FD5960018AE71 /* OnDemandWiFiControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */; }; 6FB1BD6021D2607A00A991BF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */; }; 6FB1BD6221D2607E00A991BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FB1BD6121D2607E00A991BF /* Assets.xcassets */; }; 6FB1BD9921D4BFE700A991BF /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6FB1BD9121D4BFE600A991BF /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -300,6 +302,7 @@ 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = ""; }; 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = ""; }; 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = ""; }; + 6F864769222FB87C006925D9 /* ControlRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlRow.swift; sourceTree = ""; }; 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = ""; }; 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsTracker.swift; sourceTree = ""; }; 6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandViewModel.swift; sourceTree = ""; }; @@ -312,6 +315,7 @@ 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_320x320.png; sourceTree = ""; }; 6F9B582721E8CD4300544D02 /* PopupRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupRow.swift; sourceTree = ""; }; 6FB1017821C57DE600766195 /* MockTunnels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnels.swift; sourceTree = ""; }; + 6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDemandWiFiControls.swift; sourceTree = ""; }; 6FB1BD5D21D2607A00A991BF /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 6FB1BD6121D2607E00A991BF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -443,6 +447,8 @@ 6F9B582721E8CD4300544D02 /* PopupRow.swift */, 6FE3661C21F64F6B00F78C7D /* ConfTextColorTheme.swift */, 6F5EA59A223E58A8002B380A /* ButtonRow.swift */, + 6F864769222FB87C006925D9 /* ControlRow.swift */, + 6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */, ); path = View; sourceTree = ""; @@ -1183,6 +1189,7 @@ 6B586C55220CBA6D00427C51 /* Data+KeyEncoding.swift in Sources */, 6F9B582921E8D6D100544D02 /* PopupRow.swift in Sources */, 6BAC16E6221634B300A5FB78 /* AppStorePrivacyNotice.swift in Sources */, + 6FB17946222FD5960018AE71 /* OnDemandWiFiControls.swift in Sources */, 6FB1BDBB21D50F0200A991BF /* Localizable.strings in Sources */, 6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */, 6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */, @@ -1197,6 +1204,7 @@ 6FFACD2021E4D8D500E9A2A5 /* ParseError+WireGuardAppError.swift in Sources */, 6FB1BDC021D50F0200A991BF /* NETunnelProviderProtocol+Extension.swift in Sources */, 6FBA101821D656000051C35F /* StatusMenu.swift in Sources */, + 6F86476B222FBB07006925D9 /* ControlRow.swift in Sources */, 6F613D9B21DE33B8004B217A /* KeyValueRow.swift in Sources */, 6FB1BDC121D50F0200A991BF /* String+ArrayConversion.swift in Sources */, 5F52D0BB21E3781B00283CEA /* ConfTextView.swift in Sources */, diff --git a/WireGuard/WireGuard/UI/macOS/View/ControlRow.swift b/WireGuard/WireGuard/UI/macOS/View/ControlRow.swift new file mode 100644 index 0000000..7759073 --- /dev/null +++ b/WireGuard/WireGuard/UI/macOS/View/ControlRow.swift @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + +import Cocoa + +class ControlRow: NSView { + let keyLabel: NSTextField = { + let keyLabel = NSTextField() + keyLabel.isEditable = false + keyLabel.isSelectable = false + keyLabel.isBordered = false + keyLabel.alignment = .right + keyLabel.maximumNumberOfLines = 1 + keyLabel.lineBreakMode = .byTruncatingTail + keyLabel.backgroundColor = .clear + return keyLabel + }() + + var key: String { + get { return keyLabel.stringValue } + set(value) { keyLabel.stringValue = value } + } + + override var intrinsicContentSize: NSSize { + let height = max(keyLabel.intrinsicContentSize.height, controlView.intrinsicContentSize.height) + return NSSize(width: NSView.noIntrinsicMetric, height: height) + } + + let controlView: NSView + + init(controlView: NSView) { + self.controlView = controlView + super.init(frame: CGRect.zero) + + addSubview(keyLabel) + addSubview(controlView) + keyLabel.translatesAutoresizingMaskIntoConstraints = false + controlView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor), + keyLabel.trailingAnchor.constraint(equalTo: controlView.leadingAnchor, constant: -5) + ]) + + keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal) + keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + + let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150) + widthConstraint.priority = .defaultHigh + 1 + widthConstraint.isActive = true + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + key = "" + } +} diff --git a/WireGuard/WireGuard/UI/macOS/View/OnDemandWiFiControls.swift b/WireGuard/WireGuard/UI/macOS/View/OnDemandWiFiControls.swift new file mode 100644 index 0000000..bf0e52b --- /dev/null +++ b/WireGuard/WireGuard/UI/macOS/View/OnDemandWiFiControls.swift @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + +import Cocoa + +class OnDemandWiFiControls: NSStackView { + + let onDemandWiFiCheckbox: NSButton = { + let checkbox = NSButton() + checkbox.title = tr("tunnelOnDemandWiFi") + checkbox.setButtonType(.switch) + checkbox.state = .off + return checkbox + }() + + static let onDemandSSIDOptions: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [ + .anySSID, .onlySpecificSSIDs, .exceptSpecificSSIDs + ] + + let onDemandSSIDOptionsPopup = NSPopUpButton() + + let onDemandSSIDsField: NSTokenField = { + let tokenField = NSTokenField() + tokenField.tokenizingCharacterSet = CharacterSet([]) + NSLayoutConstraint.activate([ + tokenField.widthAnchor.constraint(greaterThanOrEqualToConstant: 150) + ]) + return tokenField + }() + + override var intrinsicContentSize: NSSize { + let minHeight: CGFloat = 22 + let height = max(minHeight, onDemandWiFiCheckbox.intrinsicContentSize.height, onDemandSSIDOptionsPopup.intrinsicContentSize.height, onDemandSSIDsField.intrinsicContentSize.height) + return NSSize(width: NSView.noIntrinsicMetric, height: height) + } + + var onDemandViewModel: ActivateOnDemandViewModel? { + didSet { updateSSIDControls() } + } + + init() { + super.init(frame: CGRect.zero) + onDemandSSIDOptionsPopup.addItems(withTitles: OnDemandWiFiControls.onDemandSSIDOptions.map { $0.localizedUIString }) + setViews([onDemandWiFiCheckbox, onDemandSSIDOptionsPopup, onDemandSSIDsField], in: .leading) + orientation = .horizontal + + NSLayoutConstraint.activate([ + onDemandWiFiCheckbox.centerYAnchor.constraint(equalTo: centerYAnchor), + onDemandSSIDOptionsPopup.lastBaselineAnchor.constraint(equalTo: onDemandWiFiCheckbox.lastBaselineAnchor), + onDemandSSIDsField.lastBaselineAnchor.constraint(equalTo: onDemandWiFiCheckbox.lastBaselineAnchor) + ]) + + onDemandWiFiCheckbox.target = self + onDemandWiFiCheckbox.action = #selector(wiFiCheckboxToggled) + + onDemandSSIDOptionsPopup.target = self + onDemandSSIDOptionsPopup.action = #selector(ssidOptionsPopupValueChanged) + + updateSSIDControls() + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func saveToViewModel() { + guard let onDemandViewModel = onDemandViewModel else { return } + onDemandViewModel.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on + onDemandViewModel.ssidOption = OnDemandWiFiControls.onDemandSSIDOptions[onDemandSSIDOptionsPopup.indexOfSelectedItem] + onDemandViewModel.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? [] + } + + func updateSSIDControls() { + guard let onDemandViewModel = onDemandViewModel else { return } + onDemandWiFiCheckbox.state = onDemandViewModel.isWiFiInterfaceEnabled ? .on : .off + let optionIndex = OnDemandWiFiControls.onDemandSSIDOptions.firstIndex(of: onDemandViewModel.ssidOption) + onDemandSSIDOptionsPopup.selectItem(at: optionIndex ?? 0) + onDemandSSIDsField.objectValue = onDemandViewModel.selectedSSIDs + onDemandSSIDOptionsPopup.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled + onDemandSSIDsField.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled || onDemandViewModel.ssidOption == .anySSID + } + + @objc func wiFiCheckboxToggled() { + onDemandViewModel?.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on + updateSSIDControls() + } + + @objc func ssidOptionsPopupValueChanged() { + let selectedIndex = onDemandSSIDOptionsPopup.indexOfSelectedItem + onDemandViewModel?.ssidOption = OnDemandWiFiControls.onDemandSSIDOptions[selectedIndex] + onDemandViewModel?.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? [] + updateSSIDControls() + if !onDemandSSIDsField.isHidden { + onDemandSSIDsField.becomeFirstResponder() + } + } +} diff --git a/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift b/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift index 1c1c054..51b1944 100644 --- a/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift +++ b/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift @@ -42,12 +42,16 @@ class TunnelEditViewController: NSViewController { return textView }() - let onDemandRow: PopupRow = { - let popupRow = PopupRow() - popupRow.key = tr("macFieldOnDemand") - return popupRow + let onDemandEthernetCheckbox: NSButton = { + let checkbox = NSButton() + checkbox.title = tr("tunnelOnDemandEthernet") + checkbox.setButtonType(.switch) + checkbox.state = .off + return checkbox }() + let onDemandWiFiControls = OnDemandWiFiControls() + let scrollView: NSScrollView = { let scrollView = NSScrollView() scrollView.hasVerticalScroller = true @@ -89,6 +93,7 @@ class TunnelEditViewController: NSViewController { let tunnelsManager: TunnelsManager let tunnel: TunnelContainer? + var onDemandViewModel: ActivateOnDemandViewModel weak var delegate: TunnelEditViewControllerDelegate? @@ -101,6 +106,7 @@ class TunnelEditViewController: NSViewController { init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer?) { self.tunnelsManager = tunnelsManager self.tunnel = tunnel + self.onDemandViewModel = tunnel != nil ? ActivateOnDemandViewModel(setting: tunnel!.activateOnDemandSetting) : ActivateOnDemandViewModel() super.init(nibName: nil, bundle: nil) } @@ -109,7 +115,6 @@ class TunnelEditViewController: NSViewController { } func populateFields() { - let selectedActivateOnDemandOption: ActivateOnDemandOption if let tunnel = tunnel { // Editing an existing tunnel let tunnelConfiguration = tunnel.tunnelConfiguration! @@ -117,11 +122,6 @@ class TunnelEditViewController: NSViewController { textView.string = tunnelConfiguration.asWgQuickConfig() publicKeyRow.value = tunnelConfiguration.interface.publicKey.base64Key() ?? "" textView.privateKeyString = tunnelConfiguration.interface.privateKey.base64Key() ?? "" - if tunnel.activateOnDemandSetting.isActivateOnDemandEnabled { - selectedActivateOnDemandOption = tunnel.activateOnDemandSetting.activateOnDemandOption - } else { - selectedActivateOnDemandOption = .none - } let singlePeer = tunnelConfiguration.peers.count == 1 ? tunnelConfiguration.peers.first : nil updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: singlePeer?.allowedIPs.map { $0.stringRepresentation }) dnsServersAddedToAllowedIPs = excludePrivateIPsCheckbox.state == .on ? tunnelConfiguration.interface.dns.map { $0.stringRepresentation }.joined(separator: ", ") : nil @@ -132,7 +132,6 @@ class TunnelEditViewController: NSViewController { let bootstrappingText = "[Interface]\nPrivateKey = \(privateKey.base64Key() ?? "")\n" publicKeyRow.value = publicKey.base64Key() ?? "" textView.string = bootstrappingText - selectedActivateOnDemandOption = .none } privateKeyObservationToken = textView.observe(\.privateKeyString) { [weak publicKeyRow] textView, _ in if let privateKeyString = textView.privateKeyString, @@ -150,14 +149,25 @@ class TunnelEditViewController: NSViewController { singlePeerAllowedIPsObservationToken = textView.observe(\.singlePeerAllowedIPs) { [weak self] textView, _ in self?.updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: textView.singlePeerAllowedIPs) } - - onDemandRow.valueOptions = activateOnDemandOptions.map { TunnelViewModel.activateOnDemandOptionText(for: $0) } - onDemandRow.selectedOptionIndex = activateOnDemandOptions.firstIndex(of: selectedActivateOnDemandOption)! } override func loadView() { populateFields() + let onDemandEthernetRow = ControlRow(controlView: onDemandEthernetCheckbox) + onDemandEthernetRow.key = tr("macFieldOnDemand") + onDemandEthernetCheckbox.state = onDemandViewModel.isNonWiFiInterfaceEnabled ? .on : .off + + let onDemandWiFiRow = ControlRow(controlView: onDemandWiFiControls) + onDemandWiFiRow.key = "" + onDemandWiFiControls.onDemandViewModel = onDemandViewModel + + NSLayoutConstraint.activate([ + onDemandEthernetRow.keyLabel.firstBaselineAnchor.constraint(equalTo: onDemandEthernetRow.controlView.firstBaselineAnchor), + onDemandWiFiRow.controlView.centerYAnchor.constraint(equalTo: onDemandWiFiRow.centerYAnchor), + onDemandWiFiRow.trailingAnchor.constraint(equalTo: onDemandWiFiControls.trailingAnchor) + ]) + scrollView.documentView = textView saveButton.target = self @@ -172,7 +182,7 @@ class TunnelEditViewController: NSViewController { let margin: CGFloat = 20 let internalSpacing: CGFloat = 10 - let editorStackView = NSStackView(views: [nameRow, publicKeyRow, onDemandRow, scrollView]) + let editorStackView = NSStackView(views: [nameRow, publicKeyRow, onDemandEthernetRow, onDemandWiFiRow, scrollView]) editorStackView.orientation = .vertical editorStackView.setHuggingPriority(.defaultHigh, for: .horizontal) editorStackView.spacing = internalSpacing @@ -210,13 +220,10 @@ class TunnelEditViewController: NSViewController { ErrorPresenter.showErrorAlert(title: tr("macAlertNameIsEmpty"), message: "", from: self) return } - let onDemandSetting: ActivateOnDemandSetting - let onDemandOption = activateOnDemandOptions[onDemandRow.selectedOptionIndex] - if onDemandOption == .none { - onDemandSetting = ActivateOnDemandSetting.defaultSetting - } else { - onDemandSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: true, activateOnDemandOption: onDemandOption) - } + + onDemandViewModel.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on + onDemandWiFiControls.saveToViewModel() + let onDemandSetting = ActivateOnDemandSetting(with: onDemandViewModel.toOnDemandOption()) let isTunnelModifiedWithoutChangingName = (tunnel != nil && tunnel!.name == name) guard isTunnelModifiedWithoutChangingName || tunnelsManager.tunnel(named: name) == nil else {