diff --git a/WireGuard/WireGuard/UI/TunnelViewModel.swift b/WireGuard/WireGuard/UI/TunnelViewModel.swift index 886703e..fcbaef3 100644 --- a/WireGuard/WireGuard/UI/TunnelViewModel.swift +++ b/WireGuard/WireGuard/UI/TunnelViewModel.swift @@ -6,7 +6,7 @@ import Foundation //swiftlint:disable:next type_body_length class TunnelViewModel { - enum InterfaceField { + enum InterfaceField: CaseIterable { case name case privateKey case publicKey @@ -34,7 +34,7 @@ class TunnelViewModel { .generateKeyPair ] - enum PeerField { + enum PeerField: CaseIterable { case publicKey case preSharedKey case endpoint @@ -68,6 +68,18 @@ class TunnelViewModel { static let keyLengthInBase64 = 44 + struct ChangeHandlers { + enum FieldChange { + case added + case removed + case modified + } + var interfaceChanged: ([InterfaceField: FieldChange]) -> Void + var peerChangedAt: (Int, [PeerField: FieldChange]) -> Void + var peersRemovedAt: ([Int]) -> Void + var peersInsertedAt: ([Int]) -> Void + } + class InterfaceData { var scratchpad = [InterfaceField: String]() var fieldsWithError = Set() @@ -106,6 +118,11 @@ class TunnelViewModel { func populateScratchpad() { guard let config = validatedConfiguration else { return } guard let name = validatedName else { return } + scratchpad = TunnelViewModel.InterfaceData.createScratchPad(from: config, name: name) + } + + private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] { + var scratchpad = [InterfaceField: String]() scratchpad[.name] = name scratchpad[.privateKey] = config.privateKey.base64EncodedString() scratchpad[.publicKey] = config.publicKey.base64EncodedString() @@ -121,6 +138,7 @@ class TunnelViewModel { if !config.dns.isEmpty { scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ") } + return scratchpad } //swiftlint:disable:next cyclomatic_complexity function_body_length @@ -199,6 +217,32 @@ class TunnelViewModel { return !self[field].isEmpty } } + + func applyConfiguration(other: InterfaceConfiguration, otherName: String, changeHandler: ([InterfaceField: ChangeHandlers.FieldChange]) -> Void) { + if scratchpad.isEmpty { + populateScratchpad() + } + let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName) + var changes = [InterfaceField: ChangeHandlers.FieldChange]() + for field in InterfaceField.allCases { + switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") { + case ("", ""): + break + case ("", _): + changes[field] = .added + case (_, ""): + changes[field] = .removed + case (let this, let other): + if this != other { + changes[field] = .modified + } + } + } + scratchpad = otherScratchPad + if !changes.isEmpty { + changeHandler(changes) + } + } } class PeerData { @@ -206,6 +250,15 @@ class TunnelViewModel { var scratchpad = [PeerField: String]() var fieldsWithError = Set() var validatedConfiguration: PeerConfiguration? + var publicKey: Data? { + if let validatedConfiguration = validatedConfiguration { + return validatedConfiguration.publicKey + } + if let scratchPadPublicKey = scratchpad[.publicKey] { + return Data(base64Encoded: scratchPadPublicKey) + } + return nil + } private(set) var shouldAllowExcludePrivateIPsControl = false private(set) var shouldStronglyRecommendDNS = false @@ -241,6 +294,12 @@ class TunnelViewModel { func populateScratchpad() { guard let config = validatedConfiguration else { return } + scratchpad = TunnelViewModel.PeerData.createScratchPad(from: config) + updateExcludePrivateIPsFieldState() + } + + private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] { + var scratchpad = [PeerField: String]() scratchpad[.publicKey] = config.publicKey.base64EncodedString() if let preSharedKey = config.preSharedKey { scratchpad[.preSharedKey] = preSharedKey.base64EncodedString() @@ -263,7 +322,7 @@ class TunnelViewModel { if let lastHandshakeTime = config.lastHandshakeTime { scratchpad[.lastHandshakeTime] = prettyTimeAgo(timestamp: lastHandshakeTime) } - updateExcludePrivateIPsFieldState() + return scratchpad } //swiftlint:disable:next cyclomatic_complexity @@ -381,6 +440,30 @@ class TunnelViewModel { validatedConfiguration = nil excludePrivateIPsValue = isOn } + + func applyConfiguration(other: PeerConfiguration, peerIndex: Int, changeHandler: (Int, [PeerField: ChangeHandlers.FieldChange]) -> Void) { + if scratchpad.isEmpty { + populateScratchpad() + } + let otherScratchPad = PeerData.createScratchPad(from: other) + var changes = [PeerField: ChangeHandlers.FieldChange]() + for field in PeerField.allCases { + switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") { + case ("", ""): + break + case ("", _): + changes[field] = .added + case (_, ""): + changes[field] = .removed + case (let this, let other): + if this != other { + changes[field] = .modified + } + } + } + scratchpad = otherScratchPad + changeHandler(peerIndex, changes) + } } enum SaveResult { @@ -388,8 +471,8 @@ class TunnelViewModel { case error(String) } - var interfaceData: InterfaceData - var peersData: [PeerData] + private(set) var interfaceData: InterfaceData + private(set) var peersData: [PeerData] init(tunnelConfiguration: TunnelConfiguration?) { let interfaceData = InterfaceData() @@ -462,6 +545,55 @@ class TunnelViewModel { return .saved(tunnelConfiguration) } } + + func applyConfiguration(other: TunnelConfiguration, changeHandlers: ChangeHandlers) { + // Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering. + // Change handler callbacks are processed in the following order, which is designed to work with both the + // UITableView way (modify - delete - insert) and the NSTableView way (indices are based on past operations): + // - interfaceChanged + // - peerChangedAt + // - peersRemovedAt + // - peersInsertedAt + + interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "", changeHandler: changeHandlers.interfaceChanged) + + for otherPeer in other.peers { + if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) { + let peerData = peersData[peersDataIndex] + peerData.applyConfiguration(other: otherPeer, peerIndex: peersDataIndex, changeHandler: changeHandlers.peerChangedAt) + } + } + + var removedPeerIndices = [Int]() + for (index, peerData) in peersData.enumerated().reversed() { + if let peerPublicKey = peerData.publicKey, !other.peers.contains(where: { $0.publicKey == peerPublicKey}) { + removedPeerIndices.append(index) + peersData.remove(at: index) + } + } + if !removedPeerIndices.isEmpty { + changeHandlers.peersRemovedAt(removedPeerIndices) + } + + var addedPeerIndices = [Int]() + for otherPeer in other.peers { + if !peersData.contains(where: { $0.publicKey == otherPeer.publicKey }) { + addedPeerIndices.append(peersData.count) + let peerData = PeerData(index: peersData.count) + peerData.validatedConfiguration = otherPeer + peersData.append(peerData) + } + } + if !addedPeerIndices.isEmpty { + changeHandlers.peersInsertedAt(addedPeerIndices) + } + + for (index, peer) in peersData.enumerated() { + peer.index = index + peer.numberOfPeers = peersData.count + peer.updateExcludePrivateIPsFieldState() + } + } } extension TunnelViewModel { diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift index 6a2b1b7..beb5d24 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift @@ -13,12 +13,12 @@ class TunnelDetailTableViewController: UITableViewController { case delete } - let interfaceFields: [TunnelViewModel.InterfaceField] = [ + static let interfaceFields: [TunnelViewModel.InterfaceField] = [ .name, .publicKey, .addresses, .listenPort, .mtu, .dns ] - let peerFields: [TunnelViewModel.PeerField] = [ + static let peerFields: [TunnelViewModel.PeerField] = [ .publicKey, .preSharedKey, .endpoint, .allowedIPs, .persistentKeepAlive, .rxBytes, .txBytes, .lastHandshakeTime @@ -89,11 +89,11 @@ class TunnelDetailTableViewController: UITableViewController { } private func loadVisibleFields() { - let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields) - interfaceFieldIsVisible = interfaceFields.map { visibleInterfaceFields.contains($0) } + let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: TunnelDetailTableViewController.interfaceFields) + interfaceFieldIsVisible = TunnelDetailTableViewController.interfaceFields.map { visibleInterfaceFields.contains($0) } peerFieldIsVisible = tunnelViewModel.peersData.map { peer in - let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: peerFields) - return peerFields.map { visiblePeerFields.contains($0) } + let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: TunnelDetailTableViewController.peerFields) + return TunnelDetailTableViewController.peerFields.map { visiblePeerFields.contains($0) } } } @@ -172,13 +172,79 @@ class TunnelDetailTableViewController: UITableViewController { reloadRuntimeConfigurationTimer = nil } + func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) { + // Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering. + guard let tableView = self.tableView else { return } + let sections = self.sections + let interfaceSectionIndex = sections.firstIndex(where: { if case .interface = $0 { return true } else { return false }})! + let firstPeerSectionIndex = interfaceSectionIndex + 1 + var interfaceFieldIsVisible = self.interfaceFieldIsVisible + var peerFieldIsVisible = self.peerFieldIsVisible + + func sectionChanged(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], tableView: UITableView, section: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) { + var fieldIsVisible = fieldIsVisibleInput + var modifiedIndexPaths = [IndexPath]() + for (index, field) in fields.enumerated() where changes[field] == .modified { + let row = fieldIsVisible[0 ..< index].filter { $0 }.count + modifiedIndexPaths.append(IndexPath(row: row, section: section)) + } + if !modifiedIndexPaths.isEmpty { + tableView.reloadRows(at: modifiedIndexPaths, with: .automatic) + } + + var removedIndexPaths = [IndexPath]() + for (index, field) in fields.enumerated().reversed() where changes[field] == .removed { + let row = fieldIsVisible[0 ..< index].filter { $0 }.count + removedIndexPaths.append(IndexPath(row: row, section: section)) + fieldIsVisible[index] = false + } + if !removedIndexPaths.isEmpty { + tableView.deleteRows(at: removedIndexPaths, with: .automatic) + } + + var addedIndexPaths = [IndexPath]() + for (index, field) in fields.enumerated() where changes[field] == .added { + let row = fieldIsVisible[0 ..< index].filter { $0 }.count + addedIndexPaths.append(IndexPath(row: row, section: section)) + fieldIsVisible[index] = true + } + if !addedIndexPaths.isEmpty { + tableView.insertRows(at: addedIndexPaths, with: .automatic) + } + } + + let changeHandlers = TunnelViewModel.ChangeHandlers( + interfaceChanged: { changes in + sectionChanged(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible, + tableView: tableView, section: interfaceSectionIndex, changes: changes) + }, + peerChangedAt: { peerIndex, changes in + sectionChanged(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], + tableView: tableView, section: firstPeerSectionIndex + peerIndex, changes: changes) + }, + peersRemovedAt: { peerIndices in + let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 } + tableView.deleteSections(IndexSet(sectionIndices), with: .automatic) + }, + peersInsertedAt: { peerIndices in + let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 } + tableView.insertSections(IndexSet(sectionIndices), with: .automatic) + } + ) + + tableView.beginUpdates() + self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers) + self.loadSections() + self.loadVisibleFields() + tableView.endUpdates() + } + private func reloadRuntimeConfiguration() { - tunnel.getRuntimeTunnelConfiguration(completionHandler: { - guard let tunnelConfiguration = $0 else { return } - self.tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration) - self.loadSections() - self.tableView.reloadData() - }) + tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in + guard let tunnelConfiguration = tunnelConfiguration else { return } + guard let self = self else { return } + self.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration) + } } } @@ -261,7 +327,7 @@ extension TunnelDetailTableViewController { } private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { - let visibleInterfaceFields = interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element } + let visibleInterfaceFields = TunnelDetailTableViewController.interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element } let field = visibleInterfaceFields[indexPath.row] let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.key = field.localizedUIString @@ -270,7 +336,7 @@ extension TunnelDetailTableViewController { } private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData, peerIndex: Int) -> UITableViewCell { - let visiblePeerFields = peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element } + let visiblePeerFields = TunnelDetailTableViewController.peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element } let field = visiblePeerFields[indexPath.row] let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.key = field.localizedUIString