]> git.ipfire.org Git - thirdparty/wireguard-apple.git/commitdiff
on-demand: macOS: Support SSIDs in on demand activation
authorRoopesh Chander <roop@roopc.net>
Wed, 6 Mar 2019 10:00:42 +0000 (15:30 +0530)
committerJason A. Donenfeld <Jason@zx2c4.com>
Mon, 18 Mar 2019 05:46:55 +0000 (06:46 +0100)
Signed-off-by: Roopesh Chander <roop@roopc.net>
WireGuard/WireGuard.xcodeproj/project.pbxproj
WireGuard/WireGuard/UI/macOS/View/ControlRow.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/macOS/View/OnDemandWiFiControls.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift

index 9716b425061c1bbcc61f6ddf880fb16a1a4a7581..954b3f71389626a4aca85fd38cae83c30d6e9340 100644 (file)
@@ -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, ); }; };
                6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; };
                6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; };
                6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; };
+               6F864769222FB87C006925D9 /* ControlRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlRow.swift; sourceTree = "<group>"; };
                6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = "<group>"; };
                6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsTracker.swift; sourceTree = "<group>"; };
                6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandViewModel.swift; sourceTree = "<group>"; };
                6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_320x320.png; sourceTree = "<group>"; };
                6F9B582721E8CD4300544D02 /* PopupRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupRow.swift; sourceTree = "<group>"; };
                6FB1017821C57DE600766195 /* MockTunnels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnels.swift; sourceTree = "<group>"; };
+               6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDemandWiFiControls.swift; sourceTree = "<group>"; };
                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 = "<group>"; };
                6FB1BD6121D2607E00A991BF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
                                6F9B582721E8CD4300544D02 /* PopupRow.swift */,
                                6FE3661C21F64F6B00F78C7D /* ConfTextColorTheme.swift */,
                                6F5EA59A223E58A8002B380A /* ButtonRow.swift */,
+                               6F864769222FB87C006925D9 /* ControlRow.swift */,
+                               6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */,
                        );
                        path = View;
                        sourceTree = "<group>";
                                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 */,
                                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 (file)
index 0000000..7759073
--- /dev/null
@@ -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 (file)
index 0000000..bf0e52b
--- /dev/null
@@ -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()
+        }
+    }
+}
index 1c1c05469f1c3215fc005f66d50581bd2e912138..51b19442429760d08d1f2654ae0277b97fe19bc9 100644 (file)
@@ -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 {