From 911b16d54e7dbd432d3f7dcb7f8b78bda21f84cf Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Sat, 20 Oct 2018 19:15:53 +0530 Subject: [PATCH] Tunnel creation: Start off with tunnel creation Signed-off-by: Roopesh Chander --- WireGuard/WireGuard.xcodeproj/project.pbxproj | 4 + .../iOS/TunnelEditTableViewController.swift | 451 ++++++++++++++++++ .../iOS/TunnelsListTableViewController.swift | 6 +- 3 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index 4eb749c..3c90cbd 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 6F7774E82172020C006A79B3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; }; 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; }; 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; }; + 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; }; 6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; }; 6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; }; 6FF4AC472120B9E0002C96EB /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */; }; @@ -27,6 +28,7 @@ 6F7774E72172020C006A79B3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAddressRange.swift; sourceTree = ""; }; 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = ""; }; + 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = ""; }; 6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -62,6 +64,7 @@ 6F7774E0217181B1006A79B3 /* AppDelegate.swift */, 6F7774DF217181B1006A79B3 /* MainViewController.swift */, 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */, + 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */, ); path = iOS; sourceTree = ""; @@ -205,6 +208,7 @@ 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, 6F7774E82172020C006A79B3 /* Configuration.swift in Sources */, + 6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */, 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift new file mode 100644 index 0000000..c075faa --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/TunnelEditTableViewController.swift @@ -0,0 +1,451 @@ +// +// TunnelEditTableViewController.swift +// WireGuard +// +// Created by Roopesh Chander on 17/10/18. +// Copyright © 2018 WireGuard LLC. All rights reserved. +// + +import UIKit + +// MARK: TunnelEditTableViewController + +class TunnelEditTableViewController: UITableViewController { + + // MARK: View model + + enum InterfaceEditField: String { + case name = "Name" + case privateKey = "Private key" + case publicKey = "Public key" + case generateKeyPair = "Generate keypair" + case addresses = "Addresses" + case listenPort = "Listen port" + case mtu = "MTU" + case dns = "DNS servers" + } + + let interfaceEditFieldsBySection: [[InterfaceEditField]] = [ + [.name], + [.privateKey, .publicKey, .generateKeyPair], + [.addresses, .listenPort, .mtu, .dns] + ] + + enum PeerEditField: String { + case publicKey = "Public key" + case preSharedKey = "Pre-shared key" + case endpoint = "Endpoint" + case persistentKeepAlive = "Persistent Keepalive" + case allowedIPs = "Allowed IPs" + case excludePrivateIPs = "Exclude private IPs" + case deletePeer = "Delete peer" + } + + let peerEditFieldsBySection: [[PeerEditField]] = [ + [.publicKey, .preSharedKey, .endpoint, + .allowedIPs, .excludePrivateIPs, + .persistentKeepAlive, + .deletePeer] + ] + + // Scratchpad for entered data + + class InterfaceDataSource { + var scratchpad: [InterfaceEditField: (value: String, isValid: Bool)] = [:] + } + + class PeerDataSource { + var index: Int + var scratchpad: [PeerEditField: (value: String, isValid: Bool)] = [:] + init(index: Int) { + self.index = index + } + } + + var interfaceData: InterfaceDataSource + var peersData: [PeerDataSource] + + // MARK: TunnelEditTableViewController methods + + init() { + interfaceData = InterfaceDataSource() + peersData = [] + super.init(style: .grouped) + self.modalPresentationStyle = .formSheet + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "New configuration" + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) + + self.tableView.rowHeight = 44 + self.tableView.allowsSelection = false + + self.tableView.register(TunnelsEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelsEditTableViewKeyValueCell.id) + self.tableView.register(TunnelsEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelsEditTableViewButtonCell.id) + self.tableView.register(TunnelsEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelsEditTableViewSwitchCell.id) + } + + @objc func saveTapped() { + print("Save") + } + + @objc func cancelTapped() { + dismiss(animated: true, completion: nil) + } +} + +// MARK: UITableViewDataSource + +extension TunnelEditTableViewController { + override func numberOfSections(in tableView: UITableView) -> Int { + let numberOfInterfaceSections = interfaceEditFieldsBySection.count + let numberOfPeerSections = peerEditFieldsBySection.count + let numberOfPeers = peersData.count + + return numberOfInterfaceSections + (numberOfPeers * numberOfPeerSections) + 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let numberOfInterfaceSections = interfaceEditFieldsBySection.count + let numberOfPeerSections = peerEditFieldsBySection.count + let numberOfPeers = peersData.count + + if (section < numberOfInterfaceSections) { + // Interface + return interfaceEditFieldsBySection[section].count + } else if ((numberOfPeers > 0) && (section < (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections))) { + // Peer + let fieldIndex = (section - numberOfInterfaceSections) % numberOfPeerSections + return peerEditFieldsBySection[fieldIndex].count + } else { + // Add peer + return 1 + } + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + let numberOfInterfaceSections = interfaceEditFieldsBySection.count + let numberOfPeerSections = peerEditFieldsBySection.count + let numberOfPeers = peersData.count + + if (section < numberOfInterfaceSections) { + // Interface + return (section == 0) ? "Interface" : nil + } else if ((numberOfPeers > 0) && (section < (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections))) { + // Peer + let fieldIndex = (section - numberOfInterfaceSections) % numberOfPeerSections + return (fieldIndex == 0) ? "Peer" : nil + } else { + // Add peer + return nil + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let numberOfInterfaceSections = interfaceEditFieldsBySection.count + let numberOfPeerSections = peerEditFieldsBySection.count + let numberOfPeers = peersData.count + + let section = indexPath.section + let row = indexPath.row + + if (section < numberOfInterfaceSections) { + // Interface + let field = interfaceEditFieldsBySection[section][row] + if (field == .generateKeyPair) { + let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell + cell.buttonText = field.rawValue + cell.onTapped = { + print("Generating keypair is unimplemented") // TODO + } + return cell + } else { + let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell + cell.key = field.rawValue + switch (field) { + case .name: + cell.placeholderText = "Required" + case .privateKey: + cell.placeholderText = "Required" + case .publicKey: + cell.isValueEditable = false + case .generateKeyPair: + break + case .addresses: + break + case .listenPort: + break + case .mtu: + cell.placeholderText = "Automatic" + case .dns: + break + } + return cell + } + } else if ((numberOfPeers > 0) && (section < (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections))) { + // Peer + let peerIndex = Int((section - numberOfInterfaceSections) / numberOfPeerSections) + let peerSectionIndex = (section - numberOfInterfaceSections) % numberOfPeerSections + let peerData = peersData[peerIndex] + let field = peerEditFieldsBySection[peerSectionIndex][row] + if (field == .deletePeer) { + let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell + cell.buttonText = field.rawValue + cell.onTapped = { [weak self, weak peerData] in + guard let peerData = peerData else { return } + guard let s = self else { return } + s.showConfirmationAlert(message: "Delete this peer?", + buttonTitle: "Delete", from: cell, + onConfirmed: { [weak s] in + guard let s = s else { return } + let removedSectionIndices = s.deletePeer(peer: peerData) + s.tableView.deleteSections(removedSectionIndices, with: .automatic) + }) + } + return cell + } else if (field == .excludePrivateIPs) { + let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewSwitchCell.id, for: indexPath) as! TunnelsEditTableViewSwitchCell + cell.message = field.rawValue + return cell + } else { + let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell + cell.key = field.rawValue + switch (field) { + case .publicKey: + cell.placeholderText = "Required" + case .preSharedKey: + break + case .endpoint: + break + case .persistentKeepAlive: + cell.hasLongKey = true + break + case .allowedIPs: + break + case .excludePrivateIPs: + break + case .deletePeer: + break + } + return cell + } + } else { + assert(section == (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections)) + // Add peer + let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell + cell.buttonText = "Add peer" + cell.onTapped = { [weak self] in + guard let s = self else { return } + let addedSectionIndices = s.appendEmptyPeer() + tableView.insertSections(addedSectionIndices, with: .automatic) + } + return cell + } + } + + func appendEmptyPeer() -> IndexSet { + let numberOfInterfaceSections = interfaceEditFieldsBySection.count + let numberOfPeerSections = peerEditFieldsBySection.count + let numberOfPeers = peersData.count + + let peer = PeerDataSource(index: peersData.count) + peersData.append(peer) + + let firstAddedSectionIndex = (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections) + let addedSectionIndices = IndexSet(integersIn: firstAddedSectionIndex ..< firstAddedSectionIndex + numberOfPeerSections) + return addedSectionIndices + } + + func deletePeer(peer: PeerDataSource) -> IndexSet { + let numberOfInterfaceSections = interfaceEditFieldsBySection.count + let numberOfPeerSections = peerEditFieldsBySection.count + let numberOfPeers = peersData.count + + assert(peer.index < numberOfPeers) + + let removedPeer = peersData.remove(at: peer.index) + assert(removedPeer.index == peer.index) + for p in peersData[peer.index ..< peersData.count] { + assert(p.index > 0) + p.index = p.index - 1 + } + + let firstRemovedSectionIndex = (numberOfInterfaceSections + peer.index * numberOfPeerSections) + let removedSectionIndices = IndexSet(integersIn: firstRemovedSectionIndex ..< firstRemovedSectionIndex + numberOfPeerSections) + return removedSectionIndices + } + + func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, + onConfirmed: @escaping (() -> Void)) { + let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (action) in + onConfirmed() + } + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) + let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet) + alert.addAction(destroyAction) + alert.addAction(cancelAction) + + // popoverPresentationController will be nil on iPhone and non-nil on iPad + alert.popoverPresentationController?.sourceView = sourceView + + self.present(alert, animated: true, completion: nil) + } +} + +class TunnelsEditTableViewKeyValueCell: UITableViewCell { + static let id: String = "TunnelsEditTableViewKeyValueCell" + var key: String { + get { return keyLabel.text ?? "" } + set(value) {keyLabel.text = value } + } + var value: String { + get { return valueTextField.text ?? "" } + set(value) { valueTextField.text = value } + } + var placeholderText: String { + get { return valueTextField.placeholder ?? "" } + set(value) { valueTextField.placeholder = value } + } + var isValueEditable: Bool { + get { return valueTextField.isEnabled } + set(value) { + valueTextField.isEnabled = value + keyLabel.textColor = value ? UIColor.black : UIColor.gray + } + } + var hasLongKey: Bool { + get { return modifiableWidthRatioConstraint!.constant > 0 } + set(value) { + if (value) { + modifiableWidthRatioConstraint!.constant = 40 + } else { + modifiableWidthRatioConstraint!.constant = 0 + } + } + } + var isValueValid: Bool = true { + didSet(value) { + if (value) { + keyLabel.textColor = isValueEditable ? UIColor.black : UIColor.gray + } else { + keyLabel.textColor = UIColor.red + } + } + } + + let keyLabel: UILabel + let valueTextField: UITextField + private var modifiableWidthRatioConstraint: NSLayoutConstraint? = nil + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + keyLabel = UILabel() + valueTextField = UITextField() + super.init(style: style, reuseIdentifier: reuseIdentifier) + contentView.addSubview(keyLabel) + keyLabel.translatesAutoresizingMaskIntoConstraints = false + keyLabel.textAlignment = .right + let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width, + relatedBy: .equal, + toItem: self, attribute: .width, + multiplier: 0.4, constant: 0) + NSLayoutConstraint.activate([ + keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + keyLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 8), + widthRatioConstraint + ]) + modifiableWidthRatioConstraint = widthRatioConstraint + contentView.addSubview(valueTextField) + valueTextField.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + valueTextField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + valueTextField.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 16), + valueTextField.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -8), + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + key = "" + value = "" + placeholderText = "" + isValueEditable = true + hasLongKey = false + isValueValid = true + } +} + +class TunnelsEditTableViewButtonCell: UITableViewCell { + static let id: String = "TunnelsEditTableViewButtonCell" + var buttonText: String { + get { return button.title(for: .normal) ?? "" } + set(value) { button.setTitle(value, for: .normal) } + } + var onTapped: (() -> Void)? = nil + + let button: UIButton + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + button = UIButton(type: .system) + super.init(style: style, reuseIdentifier: reuseIdentifier) + contentView.addSubview(button) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor) + ]) + button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + } + + @objc func buttonTapped() { + onTapped?() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + buttonText = "" + onTapped = nil + } +} + +class TunnelsEditTableViewSwitchCell: UITableViewCell { + static let id: String = "TunnelsEditTableViewSwitchCell" + var message: String { + get { return textLabel?.text ?? "" } + set(value) { textLabel!.text = value } + } + var isOn: Bool { + get { return switchView.isOn } + set(value) { switchView.isOn = value } + } + + let switchView: UISwitch + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + switchView = UISwitch() + super.init(style: .default, reuseIdentifier: reuseIdentifier) + accessoryView = switchView + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + message = "" + isOn = false + } +} diff --git a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift index f9c7ff4..d58da27 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift @@ -41,7 +41,11 @@ class TunnelsListTableViewController: UITableViewController { preferredStyle: .actionSheet) alert.addAction( UIAlertAction(title: "Create from scratch", style: .default) { (action) in - print("Write") + let editVC = TunnelEditTableViewController() + let editNC = UINavigationController(rootViewController: editVC) + self.present(editNC, animated: true) { + print("Done") + } } ) alert.addAction(