diff options
author | Roopesh Chander <roop@roopc.net> | 2018-10-23 15:23:18 +0530 |
---|---|---|
committer | Roopesh Chander <roop@roopc.net> | 2018-10-27 15:13:01 +0530 |
commit | 607dd4bf3d1f844192444934d0f61cc139f96db5 (patch) | |
tree | ebcdacaff11a330399d8a68a6af01a7fe2b49f9a /WireGuard/WireGuard/UI/TunnelViewModel.swift | |
parent | Tunnel creation: Validate the data and prepare to save to a configuration (diff) | |
download | wireguard-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.swift | 309 |
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) + } + } +} |