2019-01-06 13:21:06 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
|
2019-01-08 19:22:11 +00:00
|
|
|
protocol TunnelEditViewControllerDelegate: class {
|
|
|
|
func tunnelSaved(tunnel: TunnelContainer)
|
|
|
|
func tunnelEditingCancelled()
|
|
|
|
}
|
|
|
|
|
2019-01-06 13:21:06 +00:00
|
|
|
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
|
|
|
|
}()
|
|
|
|
|
2019-01-08 19:11:36 +00:00
|
|
|
let textView: ConfTextView = {
|
2019-01-07 12:47:27 +00:00
|
|
|
let textView = ConfTextView()
|
2019-01-11 11:21:40 +00:00
|
|
|
let minWidth: CGFloat = 120
|
|
|
|
let minHeight: CGFloat = 60
|
2019-01-06 13:21:06 +00:00
|
|
|
textView.minSize = NSSize(width: 0, height: minHeight)
|
|
|
|
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
2019-01-11 11:17:20 +00:00
|
|
|
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
|
2019-01-06 13:21:06 +00:00
|
|
|
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
|
|
|
|
}()
|
|
|
|
|
2019-01-08 22:44:08 +00:00
|
|
|
let onDemandRow: PopupRow = {
|
|
|
|
let popupRow = PopupRow()
|
|
|
|
popupRow.key = tr("macFieldOnDemand")
|
|
|
|
return popupRow
|
|
|
|
}()
|
|
|
|
|
2019-01-06 13:21:06 +00:00
|
|
|
let scrollView: NSScrollView = {
|
|
|
|
let scrollView = NSScrollView()
|
|
|
|
scrollView.hasVerticalScroller = true
|
|
|
|
scrollView.borderType = .bezelBorder
|
|
|
|
return scrollView
|
|
|
|
}()
|
|
|
|
|
|
|
|
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
|
|
|
|
}()
|
|
|
|
|
2019-01-08 22:44:08 +00:00
|
|
|
let activateOnDemandOptions: [ActivateOnDemandOption] = [
|
|
|
|
.none,
|
|
|
|
.useOnDemandOverWiFiOrEthernet,
|
|
|
|
.useOnDemandOverWiFiOnly,
|
|
|
|
.useOnDemandOverEthernetOnly
|
|
|
|
]
|
|
|
|
|
2019-01-06 13:21:06 +00:00
|
|
|
let tunnelsManager: TunnelsManager
|
|
|
|
let tunnel: TunnelContainer?
|
|
|
|
|
2019-01-08 19:22:11 +00:00
|
|
|
weak var delegate: TunnelEditViewControllerDelegate?
|
|
|
|
|
2019-01-08 19:11:36 +00:00
|
|
|
var textViewObservationToken: AnyObject?
|
|
|
|
|
2019-01-06 13:21:06 +00:00
|
|
|
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer?) {
|
|
|
|
self.tunnelsManager = tunnelsManager
|
|
|
|
self.tunnel = tunnel
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
}
|
|
|
|
|
2019-01-08 19:47:46 +00:00
|
|
|
func populateTextFields() {
|
2019-01-08 22:44:08 +00:00
|
|
|
let selectedActivateOnDemandOption: ActivateOnDemandOption
|
2019-01-08 19:47:46 +00:00
|
|
|
if let tunnel = tunnel {
|
|
|
|
// Editing an existing tunnel
|
|
|
|
let tunnelConfiguration = tunnel.tunnelConfiguration!
|
2019-01-06 13:21:06 +00:00
|
|
|
nameRow.value = tunnel.name
|
|
|
|
textView.string = tunnelConfiguration.asWgQuickConfig()
|
2019-01-08 19:11:36 +00:00
|
|
|
publicKeyRow.value = tunnelConfiguration.interface.publicKey.base64EncodedString()
|
|
|
|
textView.privateKeyString = tunnelConfiguration.interface.privateKey.base64EncodedString()
|
|
|
|
textViewObservationToken = textView.observe(\.privateKeyString) { [weak publicKeyRow] textView, _ in
|
|
|
|
if let privateKeyString = textView.privateKeyString,
|
|
|
|
let privateKey = Data(base64Encoded: privateKeyString),
|
|
|
|
privateKey.count == TunnelConfiguration.keyLength {
|
|
|
|
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
|
|
|
publicKeyRow?.value = publicKey.base64EncodedString()
|
|
|
|
} else {
|
|
|
|
publicKeyRow?.value = ""
|
|
|
|
}
|
|
|
|
}
|
2019-01-08 22:44:08 +00:00
|
|
|
if tunnel.activateOnDemandSetting.isActivateOnDemandEnabled {
|
|
|
|
selectedActivateOnDemandOption = tunnel.activateOnDemandSetting.activateOnDemandOption
|
|
|
|
} else {
|
|
|
|
selectedActivateOnDemandOption = .none
|
|
|
|
}
|
2019-01-08 19:47:46 +00:00
|
|
|
} else {
|
|
|
|
// Creating a new tunnel
|
|
|
|
let privateKey = Curve25519.generatePrivateKey()
|
|
|
|
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
|
|
|
|
let bootstrappingText = """
|
|
|
|
[Interface]
|
|
|
|
PrivateKey = \(privateKey.base64EncodedString())
|
|
|
|
"""
|
|
|
|
publicKeyRow.value = publicKey.base64EncodedString()
|
|
|
|
textView.string = bootstrappingText
|
2019-01-08 22:44:08 +00:00
|
|
|
selectedActivateOnDemandOption = .none
|
2019-01-06 13:21:06 +00:00
|
|
|
}
|
2019-01-08 22:44:08 +00:00
|
|
|
|
|
|
|
onDemandRow.valueOptions = activateOnDemandOptions.map {
|
|
|
|
return TunnelViewModel.activateOnDemandOptionText(for: $0)
|
|
|
|
}
|
|
|
|
onDemandRow.selectedOptionIndex = activateOnDemandOptions.firstIndex(of: selectedActivateOnDemandOption)!
|
2019-01-08 19:47:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
override func loadView() {
|
|
|
|
populateTextFields()
|
2019-01-06 13:21:06 +00:00
|
|
|
|
|
|
|
scrollView.documentView = textView
|
|
|
|
|
|
|
|
saveButton.target = self
|
|
|
|
saveButton.action = #selector(saveButtonClicked)
|
|
|
|
|
|
|
|
discardButton.target = self
|
|
|
|
discardButton.action = #selector(discardButtonClicked)
|
|
|
|
|
|
|
|
let margin: CGFloat = 20
|
|
|
|
let internalSpacing: CGFloat = 10
|
|
|
|
|
2019-01-08 22:44:08 +00:00
|
|
|
let editorStackView = NSStackView(views: [nameRow, publicKeyRow, onDemandRow, scrollView])
|
2019-01-06 13:21:06 +00:00
|
|
|
editorStackView.orientation = .vertical
|
|
|
|
editorStackView.setHuggingPriority(.defaultHigh, for: .horizontal)
|
|
|
|
editorStackView.spacing = internalSpacing
|
|
|
|
|
|
|
|
let buttonRowStackView = NSStackView()
|
|
|
|
buttonRowStackView.setViews([discardButton, saveButton], in: .trailing)
|
|
|
|
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)
|
|
|
|
])
|
2019-01-11 11:21:40 +00:00
|
|
|
containerView.frame = NSRect(x: 0, y: 0, width: 600, height: 480)
|
2019-01-06 13:21:06 +00:00
|
|
|
|
|
|
|
self.view = containerView
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func saveButtonClicked() {
|
2019-01-08 13:28:40 +00:00
|
|
|
let name = nameRow.value
|
|
|
|
guard !name.isEmpty else {
|
|
|
|
ErrorPresenter.showErrorAlert(title: tr("macAlertNameIsEmpty"), message: "", from: self)
|
|
|
|
return
|
|
|
|
}
|
2019-01-08 22:44:08 +00:00
|
|
|
let onDemandSetting: ActivateOnDemandSetting
|
|
|
|
let onDemandOption = activateOnDemandOptions[onDemandRow.selectedOptionIndex]
|
|
|
|
if onDemandOption == .none {
|
|
|
|
onDemandSetting = ActivateOnDemandSetting.defaultSetting
|
|
|
|
} else {
|
|
|
|
onDemandSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: true, activateOnDemandOption: onDemandOption)
|
|
|
|
}
|
2019-01-08 13:28:40 +00:00
|
|
|
if let tunnel = tunnel {
|
|
|
|
// We're modifying an existing tunnel
|
|
|
|
if name != tunnel.name && tunnelsManager.tunnel(named: name) != nil {
|
|
|
|
ErrorPresenter.showErrorAlert(title: tr(format: "macAlertDuplicateName (%@)", name), message: "", from: self)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
do {
|
2019-01-08 18:06:27 +00:00
|
|
|
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value)
|
2019-01-08 13:28:40 +00:00
|
|
|
tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: onDemandSetting) { [weak self] error in
|
|
|
|
if let error = error {
|
|
|
|
ErrorPresenter.showErrorAlert(error: error, from: self)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self?.dismiss(self)
|
2019-01-08 19:22:11 +00:00
|
|
|
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
2019-01-08 13:28:40 +00:00
|
|
|
}
|
|
|
|
} catch let error as WireGuardAppError {
|
|
|
|
ErrorPresenter.showErrorAlert(error: error, from: self)
|
|
|
|
} catch {
|
|
|
|
fatalError()
|
|
|
|
}
|
2019-01-08 19:47:46 +00:00
|
|
|
} else {
|
|
|
|
// We're creating a new tunnel
|
|
|
|
if tunnelsManager.tunnel(named: name) != nil {
|
|
|
|
ErrorPresenter.showErrorAlert(title: tr(format: "macAlertDuplicateName (%@)", name), message: "", from: self)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value)
|
|
|
|
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: onDemandSetting) { [weak self] result in
|
|
|
|
if let error = result.error {
|
|
|
|
ErrorPresenter.showErrorAlert(error: error, from: self)
|
|
|
|
} else {
|
|
|
|
let tunnel: TunnelContainer = result.value!
|
|
|
|
self?.dismiss(self)
|
|
|
|
self?.delegate?.tunnelSaved(tunnel: tunnel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch let error as WireGuardAppError {
|
|
|
|
ErrorPresenter.showErrorAlert(error: error, from: self)
|
|
|
|
} catch {
|
|
|
|
fatalError()
|
|
|
|
}
|
2019-01-08 13:28:40 +00:00
|
|
|
}
|
2019-01-06 13:21:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func discardButtonClicked() {
|
2019-01-08 19:22:11 +00:00
|
|
|
delegate?.tunnelEditingCancelled()
|
2019-01-06 13:21:06 +00:00
|
|
|
dismiss(self)
|
|
|
|
}
|
|
|
|
}
|