aboutsummaryrefslogblamecommitdiffstats
path: root/WireGuard/WireGuard/UI/macOS/ViewController/TunnelEditViewController.swift
blob: 3d959cfbab4a047941aa4a1e16fa97c0236f2cc9 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                               
                                                             


            




                                                  













                                                                                                                     
                                  
                                     
                                   
                                  

                                                                                                                  


                                                                                        










                                                                                                 
                                                   
 


                                             
                                            



                                            







                                                          

















                                              
                                                    
 

                                                        

                                              
                                                        
 

                                            


                                                                    
                                                                                                                         






                                                           
                           


                                                                 
                                       
                                                                   

                                                                                                  

                                                                                                                           
                                                                                                                                                                                  



                                                                                    

                                                                                                 
                                               
         
                                                                                                              
                                                                
                                                                   

                                                                                        
                                                                 



                                        


                                                                                                  


                                                                                                                    


                              
                        



                                          
                                                       

                                   
                                                             
 


                                                                                               

                                                                 


                                         
                                                                                                          





                                                                               
                                                                           












                                                                                                         
                                                                         



                                 



                                                     
                                                                  

     
                                   




                                                                                                    
 
                                             
                                                                 






                                                                                                                         
                                                    








                                                                                                                    













                                                                                                                                                                                            

                                        

                                                 
                                                                                                                                                  
                                                     


                                                                           
                 

                                                           
             

                                          
                                                                                         
                                                                                                                                          
                                                         






                                                                               
                 
             
         

     
                                      
                                          

                     

















                                                                                                                                                                                                    
                                                                                                                                                                               

                                                                   
                                                                                          

         
 
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.

import Cocoa

protocol TunnelEditViewControllerDelegate: class {
    func tunnelSaved(tunnel: TunnelContainer)
    func tunnelEditingCancelled()
}

class TunnelEditViewController: NSViewController {

    let nameRow: EditableKeyValueRow = {
        let nameRow = EditableKeyValueRow()
        nameRow.key = tr(format: "macFieldKey (%@)", TunnelViewModel.InterfaceField.name.localizedUIString)
        return nameRow
    }()

    let publicKeyRow: KeyValueRow = {
        let publicKeyRow = KeyValueRow()
        publicKeyRow.key = tr(format: "macFieldKey (%@)", TunnelViewModel.InterfaceField.publicKey.localizedUIString)
        return publicKeyRow
    }()

    let textView: ConfTextView = {
        let textView = ConfTextView()
        let minWidth: CGFloat = 120
        let minHeight: CGFloat = 0
        textView.minSize = NSSize(width: 0, height: minHeight)
        textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
        textView.autoresizingMask = [.width] // Width should be based on superview width
        textView.isHorizontallyResizable = false // Width shouldn't be based on content
        textView.isVerticallyResizable = true // Height should be based on content
        if let textContainer = textView.textContainer {
            textContainer.size = NSSize(width: minWidth, height: CGFloat.greatestFiniteMagnitude)
            textContainer.widthTracksTextView = true
        }
        NSLayoutConstraint.activate([
            textView.widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth),
            textView.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
        ])
        return textView
    }()

    let onDemandControlsRow = OnDemandControlsRow()

    let scrollView: NSScrollView = {
        let scrollView = NSScrollView()
        scrollView.hasVerticalScroller = true
        scrollView.autohidesScrollers = true
        scrollView.borderType = .bezelBorder
        return scrollView
    }()

    let excludePrivateIPsCheckbox: NSButton = {
        let checkbox = NSButton()
        checkbox.title = tr("tunnelPeerExcludePrivateIPs")
        checkbox.setButtonType(.switch)
        checkbox.state = .off
        return checkbox
    }()

    let discardButton: NSButton = {
        let button = NSButton()
        button.title = tr("macEditDiscard")
        button.setButtonType(.momentaryPushIn)
        button.bezelStyle = .rounded
        return button
    }()

    let saveButton: NSButton = {
        let button = NSButton()
        button.title = tr("macEditSave")
        button.setButtonType(.momentaryPushIn)
        button.bezelStyle = .rounded
        return button
    }()

    let tunnelsManager: TunnelsManager
    let tunnel: TunnelContainer?
    var onDemandViewModel: ActivateOnDemandViewModel

    weak var delegate: TunnelEditViewControllerDelegate?

    var privateKeyObservationToken: AnyObject?
    var hasErrorObservationToken: AnyObject?
    var singlePeerAllowedIPsObservationToken: AnyObject?

    var dnsServersAddedToAllowedIPs: String?

    init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer?) {
        self.tunnelsManager = tunnelsManager
        self.tunnel = tunnel
        self.onDemandViewModel = tunnel != nil ? ActivateOnDemandViewModel(tunnel: tunnel!) : ActivateOnDemandViewModel()
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func populateFields() {
        if let tunnel = tunnel {
            // Editing an existing tunnel
            let tunnelConfiguration = tunnel.tunnelConfiguration!
            nameRow.value = tunnel.name
            textView.string = tunnelConfiguration.asWgQuickConfig()
            publicKeyRow.value = tunnelConfiguration.interface.publicKey.base64Key() ?? ""
            textView.privateKeyString = tunnelConfiguration.interface.privateKey.base64Key() ?? ""
            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
        } else {
            // Creating a new tunnel
            let privateKey = Curve25519.generatePrivateKey()
            let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
            let bootstrappingText = "[Interface]\nPrivateKey = \(privateKey.base64Key() ?? "")\n"
            publicKeyRow.value = publicKey.base64Key() ?? ""
            textView.string = bootstrappingText
        }
        privateKeyObservationToken = textView.observe(\.privateKeyString) { [weak publicKeyRow] textView, _ in
            if let privateKeyString = textView.privateKeyString,
                let privateKey = Data(base64Key: privateKeyString),
                privateKey.count == TunnelConfiguration.keyLength {
                let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
                publicKeyRow?.value = publicKey.base64Key() ?? ""
            } else {
                publicKeyRow?.value = ""
            }
        }
        hasErrorObservationToken = textView.observe(\.hasError) { [weak saveButton] textView, _ in
            saveButton?.isEnabled = !textView.hasError
        }
        singlePeerAllowedIPsObservationToken = textView.observe(\.singlePeerAllowedIPs) { [weak self] textView, _ in
            self?.updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: textView.singlePeerAllowedIPs)
        }
    }

    override func loadView() {
        populateFields()

        scrollView.documentView = textView

        saveButton.target = self
        saveButton.action = #selector(handleSaveAction)

        discardButton.target = self
        discardButton.action = #selector(handleDiscardAction)

        excludePrivateIPsCheckbox.target = self
        excludePrivateIPsCheckbox.action = #selector(excludePrivateIPsCheckboxToggled(sender:))

        onDemandControlsRow.onDemandViewModel = onDemandViewModel

        let margin: CGFloat = 20
        let internalSpacing: CGFloat = 10

        let editorStackView = NSStackView(views: [nameRow, publicKeyRow, onDemandControlsRow, scrollView])
        editorStackView.orientation = .vertical
        editorStackView.setHuggingPriority(.defaultHigh, for: .horizontal)
        editorStackView.spacing = internalSpacing

        let buttonRowStackView = NSStackView()
        buttonRowStackView.setViews([discardButton, saveButton], in: .trailing)
        buttonRowStackView.addView(excludePrivateIPsCheckbox, in: .leading)
        buttonRowStackView.orientation = .horizontal
        buttonRowStackView.spacing = internalSpacing

        let containerView = NSStackView(views: [editorStackView, buttonRowStackView])
        containerView.orientation = .vertical
        containerView.edgeInsets = NSEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
        containerView.setHuggingPriority(.defaultHigh, for: .horizontal)
        containerView.spacing = internalSpacing

        NSLayoutConstraint.activate([
            containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 180),
            containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
        ])
        containerView.frame = NSRect(x: 0, y: 0, width: 600, height: 480)

        self.view = containerView
    }

    func setUserInteractionEnabled(_ enabled: Bool) {
        view.window?.ignoresMouseEvents = !enabled
        nameRow.valueLabel.isEditable = enabled
        textView.isEditable = enabled
        onDemandControlsRow.onDemandSSIDsField.isEnabled = enabled
    }

    @objc func handleSaveAction() {
        let name = nameRow.value
        guard !name.isEmpty else {
            ErrorPresenter.showErrorAlert(title: tr("macAlertNameIsEmpty"), message: "", from: self)
            return
        }

        onDemandControlsRow.saveToViewModel()
        let onDemandOption = onDemandViewModel.toOnDemandOption()

        let isTunnelModifiedWithoutChangingName = (tunnel != nil && tunnel!.name == name)
        guard isTunnelModifiedWithoutChangingName || tunnelsManager.tunnel(named: name) == nil else {
            ErrorPresenter.showErrorAlert(title: tr(format: "macAlertDuplicateName (%@)", name), message: "", from: self)
            return
        }

        var tunnelConfiguration: TunnelConfiguration
        do {
            tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value)
        } catch let error as WireGuardAppError {
            ErrorPresenter.showErrorAlert(error: error, from: self)
            return
        } catch {
            fatalError()
        }

        if excludePrivateIPsCheckbox.state == .on, tunnelConfiguration.peers.count == 1, let dnsServersAddedToAllowedIPs = dnsServersAddedToAllowedIPs {
            // Update the DNS servers in the AllowedIPs
            let tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
            let originalAllowedIPs = tunnelViewModel.peersData[0][.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
            let dnsServersInAllowedIPs =  TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(dnsServersAddedToAllowedIPs.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
            let dnsServersCurrent =  TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(tunnelViewModel.interfaceData[.dns].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
            let modifiedAllowedIPs = originalAllowedIPs.filter { !dnsServersInAllowedIPs.contains($0) } + dnsServersCurrent
            tunnelViewModel.peersData[0][.allowedIPs] = modifiedAllowedIPs.joined(separator: ", ")
            let saveResult = tunnelViewModel.save()
            if case .saved(let modifiedTunnelConfiguration) = saveResult {
                tunnelConfiguration = modifiedTunnelConfiguration
            }
        }

        setUserInteractionEnabled(false)

        if let tunnel = tunnel {
            // We're modifying an existing tunnel
            tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] error in
                self?.setUserInteractionEnabled(true)
                if let error = error {
                    ErrorPresenter.showErrorAlert(error: error, from: self)
                    return
                }
                self?.dismiss(self)
                self?.delegate?.tunnelSaved(tunnel: tunnel)
            }
        } else {
            // We're creating a new tunnel
            AppStorePrivacyNotice.show(from: self, into: tunnelsManager) { [weak self] in
                self?.tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] result in
                    self?.setUserInteractionEnabled(true)
                    if let error = result.error {
                        ErrorPresenter.showErrorAlert(error: error, from: self)
                        return
                    }
                    let tunnel: TunnelContainer = result.value!
                    self?.dismiss(self)
                    self?.delegate?.tunnelSaved(tunnel: tunnel)
                }
            }
        }
    }

    @objc func handleDiscardAction() {
        delegate?.tunnelEditingCancelled()
        dismiss(self)
    }

    func updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: [String]?) {
        let shouldAllowExcludePrivateIPsControl: Bool
        let excludePrivateIPsValue: Bool
        if let singlePeerAllowedIPs = singlePeerAllowedIPs {
            (shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: true, allowedIPs: Set<String>(singlePeerAllowedIPs))
        } else {
            (shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: false, allowedIPs: Set<String>())
        }
        excludePrivateIPsCheckbox.isHidden = !shouldAllowExcludePrivateIPsControl
        excludePrivateIPsCheckbox.state = excludePrivateIPsValue ? .on : .off
    }

    @objc func excludePrivateIPsCheckboxToggled(sender: AnyObject?) {
        guard let excludePrivateIPsCheckbox = sender as? NSButton else { return }
        guard let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value) else { return }
        let isOn = excludePrivateIPsCheckbox.state == .on
        let tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
        tunnelViewModel.peersData.first?.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: tunnelViewModel.interfaceData[.dns], oldDNSServers: dnsServersAddedToAllowedIPs)
        if let modifiedConfig = tunnelViewModel.asWgQuickConfig() {
            textView.setConfText(modifiedConfig)
            dnsServersAddedToAllowedIPs = isOn ? tunnelViewModel.interfaceData[.dns] : nil
        }
    }
}