297 lines
13 KiB
Swift
297 lines
13 KiB
Swift
// SPDX-License-Identifier: MIT
|
|
// Copyright © 2018-2020 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
|
|
button.keyEquivalent = "s"
|
|
button.keyEquivalentModifierMask = [.command]
|
|
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.privateKey.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 = PrivateKey()
|
|
let bootstrappingText = "[Interface]\nPrivateKey = \(privateKey.base64Key)\n"
|
|
publicKeyRow.value = privateKey.publicKey.base64Key
|
|
textView.string = bootstrappingText
|
|
updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: nil)
|
|
dnsServersAddedToAllowedIPs = nil
|
|
}
|
|
privateKeyObservationToken = textView.observe(\.privateKeyString) { [weak publicKeyRow] textView, _ in
|
|
if let privateKeyString = textView.privateKeyString,
|
|
let privateKey = PrivateKey(base64Key: privateKeyString) {
|
|
publicKeyRow?.value = privateKey.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
|
|
guard let self = self else { return }
|
|
self.setUserInteractionEnabled(true)
|
|
if let error = error {
|
|
ErrorPresenter.showErrorAlert(error: error, from: self)
|
|
return
|
|
}
|
|
self.delegate?.tunnelSaved(tunnel: tunnel)
|
|
self.presentingViewController?.dismiss(self)
|
|
}
|
|
} else {
|
|
// We're creating a new tunnel
|
|
self.tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] result in
|
|
guard let self = self else { return }
|
|
self.setUserInteractionEnabled(true)
|
|
switch result {
|
|
case .failure(let error):
|
|
ErrorPresenter.showErrorAlert(error: error, from: self)
|
|
case .success(let tunnel):
|
|
self.delegate?.tunnelSaved(tunnel: tunnel)
|
|
self.presentingViewController?.dismiss(self)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func handleDiscardAction() {
|
|
delegate?.tunnelEditingCancelled()
|
|
presentingViewController?.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
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TunnelEditViewController {
|
|
override func cancelOperation(_ sender: Any?) {
|
|
handleDiscardAction()
|
|
}
|
|
}
|