aboutsummaryrefslogtreecommitdiffstats
path: root/WireGuard/WireGuard/UI/TunnelViewModel.swift
diff options
context:
space:
mode:
authorRoopesh Chander <roop@roopc.net>2018-10-23 15:23:18 +0530
committerRoopesh Chander <roop@roopc.net>2018-10-27 15:13:01 +0530
commit607dd4bf3d1f844192444934d0f61cc139f96db5 (patch)
treeebcdacaff11a330399d8a68a6af01a7fe2b49f9a /WireGuard/WireGuard/UI/TunnelViewModel.swift
parentTunnel creation: Validate the data and prepare to save to a configuration (diff)
downloadwireguard-apple-607dd4bf3d1f844192444934d0f61cc139f96db5.tar.xz
wireguard-apple-607dd4bf3d1f844192444934d0f61cc139f96db5.zip
Tunnel creation: Refactor by creating a separate view model
Signed-off-by: Roopesh Chander <roop@roopc.net>
Diffstat (limited to 'WireGuard/WireGuard/UI/TunnelViewModel.swift')
-rw-r--r--WireGuard/WireGuard/UI/TunnelViewModel.swift309
1 files changed, 309 insertions, 0 deletions
diff --git a/WireGuard/WireGuard/UI/TunnelViewModel.swift b/WireGuard/WireGuard/UI/TunnelViewModel.swift
new file mode 100644
index 0000000..6b6285c
--- /dev/null
+++ b/WireGuard/WireGuard/UI/TunnelViewModel.swift
@@ -0,0 +1,309 @@
+//
+// TunnelViewModel.swift
+// WireGuard
+//
+// Created by Roopesh Chander on 23/10/18.
+// Copyright © 2018 WireGuard LLC. All rights reserved.
+//
+
+import UIKit
+
+class TunnelViewModel {
+
+ 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"
+ }
+
+ 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"
+ }
+
+ class InterfaceData {
+ var scratchpad: [InterfaceEditField: String] = [:]
+ var fieldsWithError: Set<InterfaceEditField> = []
+ var validatedConfiguration: InterfaceConfiguration? = nil
+
+ subscript(field: InterfaceEditField) -> String {
+ get {
+ if (scratchpad.isEmpty) {
+ // When starting to read a config, setup the scratchpad.
+ // The scratchpad shall serve as a cache of what we want to show in the UI.
+ populateScratchpad()
+ }
+ return scratchpad[field] ?? ""
+ }
+ set(stringValue) {
+ if (scratchpad.isEmpty) {
+ // When starting to edit a config, setup the scratchpad and remove the configuration.
+ // The scratchpad shall be the sole source of the being-edited configuration.
+ populateScratchpad()
+ }
+ validatedConfiguration = nil
+ if (stringValue.isEmpty) {
+ scratchpad.removeValue(forKey: field)
+ } else {
+ scratchpad[field] = stringValue
+ }
+ }
+ }
+
+ func populateScratchpad() {
+ // Populate the scratchpad from the configuration object
+ guard let config = validatedConfiguration else { return }
+ scratchpad[.name] = config.name
+ scratchpad[.privateKey] = config.privateKey.base64EncodedString()
+ if (!config.addresses.isEmpty) {
+ scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
+ }
+ if let listenPort = config.listenPort {
+ scratchpad[.listenPort] = String(listenPort)
+ }
+ if let mtu = config.mtu {
+ scratchpad[.mtu] = String(mtu)
+ }
+ if let dns = config.dns {
+ scratchpad[.dns] = String(dns)
+ }
+ }
+
+ func save() -> SaveResult<InterfaceConfiguration> {
+ fieldsWithError.removeAll()
+ guard let name = scratchpad[.name] else {
+ fieldsWithError.insert(.name)
+ return .error("Interface name is required")
+ }
+ guard let privateKeyString = scratchpad[.privateKey] else {
+ fieldsWithError.insert(.privateKey)
+ return .error("Interface's private key is required")
+ }
+ guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else {
+ fieldsWithError.insert(.privateKey)
+ return .error("Interface's private key should be a 32-byte key in base64 encoding")
+ }
+ var config = InterfaceConfiguration(name: name, privateKey: privateKey)
+ var errorMessages: [String] = []
+ if let addressesString = scratchpad[.addresses] {
+ var addresses: [IPAddressRange] = []
+ for addressString in addressesString.split(separator: ",") {
+ let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
+ if let address = IPAddressRange(from: trimmedString) {
+ addresses.append(address)
+ } else {
+ fieldsWithError.insert(.addresses)
+ errorMessages.append("Interface addresses should be a list of comma-separated IP addresses in CIDR notation")
+ }
+ }
+ config.addresses = addresses
+ }
+ if let listenPortString = scratchpad[.listenPort] {
+ if let listenPort = UInt64(listenPortString) {
+ config.listenPort = listenPort
+ } else {
+ fieldsWithError.insert(.listenPort)
+ errorMessages.append("Interface's listen port should be a number")
+ }
+ }
+ if let mtuString = scratchpad[.mtu] {
+ if let mtu = UInt64(mtuString) {
+ config.mtu = mtu
+ } else {
+ fieldsWithError.insert(.mtu)
+ errorMessages.append("Interface's MTU should be a number")
+ }
+ }
+ // TODO: Validate DNS
+ if let dnsString = scratchpad[.dns] {
+ config.dns = dnsString
+ }
+
+ guard (errorMessages.isEmpty) else {
+ return .error(errorMessages.first!)
+ }
+ validatedConfiguration = config
+ return .saved(config)
+ }
+ }
+
+ class PeerData {
+ var index: Int
+ var scratchpad: [PeerEditField: String] = [:]
+ var fieldsWithError: Set<PeerEditField> = []
+ var validatedConfiguration: PeerConfiguration? = nil
+
+ init(index: Int) {
+ self.index = index
+ }
+
+ subscript(field: PeerEditField) -> String {
+ get {
+ if (scratchpad.isEmpty) {
+ // When starting to read a config, setup the scratchpad.
+ // The scratchpad shall serve as a cache of what we want to show in the UI.
+ populateScratchpad()
+ }
+ return scratchpad[field] ?? ""
+ }
+ set(stringValue) {
+ if (scratchpad.isEmpty) {
+ // When starting to edit a config, setup the scratchpad and remove the configuration.
+ // The scratchpad shall be the sole source of the being-edited configuration.
+ populateScratchpad()
+ }
+ validatedConfiguration = nil
+ if (stringValue.isEmpty) {
+ scratchpad.removeValue(forKey: field)
+ } else {
+ scratchpad[field] = stringValue
+ }
+ }
+ }
+
+ func populateScratchpad() {
+ // Populate the scratchpad from the configuration object
+ guard let config = validatedConfiguration else { return }
+ scratchpad[.publicKey] = config.publicKey.base64EncodedString()
+ if let preSharedKey = config.preSharedKey {
+ scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
+ }
+ if (!config.allowedIPs.isEmpty) {
+ scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
+ }
+ if let endpoint = config.endpoint {
+ scratchpad[.endpoint] = endpoint.stringRepresentation()
+ }
+ if let persistentKeepAlive = config.persistentKeepAlive {
+ scratchpad[.persistentKeepAlive] = String(persistentKeepAlive)
+ }
+ }
+
+ func save() -> SaveResult<PeerConfiguration> {
+ fieldsWithError.removeAll()
+ guard let publicKeyString = scratchpad[.publicKey] else {
+ fieldsWithError.insert(.publicKey)
+ return .error("Peer's public key is required")
+ }
+ guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else {
+ fieldsWithError.insert(.publicKey)
+ return .error("Peer's public key should be a 32-byte key in base64 encoding")
+ }
+ var config = PeerConfiguration(publicKey: publicKey)
+ var errorMessages: [String] = []
+ if let preSharedKeyString = scratchpad[.preSharedKey] {
+ if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 {
+ config.preSharedKey = preSharedKey
+ } else {
+ fieldsWithError.insert(.preSharedKey)
+ errorMessages.append("Peer's pre-shared key should be a 32-byte key in base64 encoding")
+ }
+ }
+ if let allowedIPsString = scratchpad[.allowedIPs] {
+ var allowedIPs: [IPAddressRange] = []
+ for allowedIPString in allowedIPsString.split(separator: ",") {
+ let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
+ if let allowedIP = IPAddressRange(from: trimmedString) {
+ allowedIPs.append(allowedIP)
+ } else {
+ fieldsWithError.insert(.allowedIPs)
+ errorMessages.append("Peer's allowedIPs should be a list of comma-separated IP addresses in CIDR notation")
+ }
+ }
+ config.allowedIPs = allowedIPs
+ }
+ if let endpointString = scratchpad[.endpoint] {
+ if let endpoint = Endpoint(from: endpointString) {
+ config.endpoint = endpoint
+ } else {
+ fieldsWithError.insert(.endpoint)
+ errorMessages.append("Peer's endpoint should be of the form 'host:port' or '[host]:port'")
+ }
+ }
+ if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] {
+ if let persistentKeepAlive = UInt64(persistentKeepAliveString) {
+ config.persistentKeepAlive = persistentKeepAlive
+ } else {
+ fieldsWithError.insert(.persistentKeepAlive)
+ errorMessages.append("Peer's persistent keepalive should be a number")
+ }
+ }
+
+ guard (errorMessages.isEmpty) else {
+ return .error(errorMessages.first!)
+ }
+ validatedConfiguration = config
+ return .saved(config)
+ }
+ }
+
+ enum SaveResult<Configuration> {
+ case saved(Configuration)
+ case error(String) // TODO: Localize error messages
+ }
+
+ var interfaceData: InterfaceData
+ var peersData: [PeerData]
+
+ init(tunnelConfiguration: TunnelConfiguration?) {
+ interfaceData = InterfaceData()
+ peersData = []
+ if let tunnelConfiguration = tunnelConfiguration {
+ interfaceData.validatedConfiguration = tunnelConfiguration.interface
+ for (i, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
+ let peerData = PeerData(index: i)
+ peerData.validatedConfiguration = peerConfiguration
+ peersData.append(peerData)
+ }
+ }
+ }
+
+ func appendEmptyPeer() {
+ let peer = PeerData(index: peersData.count)
+ peersData.append(peer)
+ }
+
+ func deletePeer(peer: PeerData) {
+ 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
+ }
+ }
+
+ func save() -> SaveResult<TunnelConfiguration> {
+ // Attempt to save the interface and all peers, so that all erroring fields are collected
+ let interfaceSaveResult = interfaceData.save()
+ let peerSaveResults = peersData.map { $0.save() }
+ // Collate the results
+ switch (interfaceSaveResult) {
+ case .error(let errorMessage):
+ return .error(errorMessage)
+ case .saved(let interfaceConfiguration):
+ var peerConfigurations: [PeerConfiguration] = []
+ peerConfigurations.reserveCapacity(peerSaveResults.count)
+ for peerSaveResult in peerSaveResults {
+ switch (peerSaveResult) {
+ case .error(let errorMessage):
+ return .error(errorMessage)
+ case .saved(let peerConfiguration):
+ peerConfigurations.append(peerConfiguration)
+ }
+ }
+ let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
+ tunnelConfiguration.peers = peerConfigurations
+ return .saved(tunnelConfiguration)
+ }
+ }
+}