Tunnel creation: Refactor by creating a separate view model
This commit is contained in:
parent
9f252d4e37
commit
0fa97c38ed
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; };
|
||||
6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
|
||||
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; };
|
||||
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; };
|
||||
|
@ -21,6 +22,7 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = "<group>"; };
|
||||
6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
|
||||
6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
|
||||
6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -54,6 +56,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
6F7774DE217181B1006A79B3 /* iOS */,
|
||||
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */,
|
||||
);
|
||||
path = UI;
|
||||
sourceTree = "<group>";
|
||||
|
@ -206,6 +209,7 @@
|
|||
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
|
||||
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
|
||||
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
|
||||
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
|
||||
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
|
||||
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
|
||||
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,256 +12,23 @@ import UIKit
|
|||
|
||||
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]] = [
|
||||
let interfaceEditFieldsBySection: [[TunnelViewModel.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]] = [
|
||||
let peerEditFieldsBySection: [[TunnelViewModel.PeerEditField]] = [
|
||||
[.publicKey, .preSharedKey, .endpoint,
|
||||
.allowedIPs, .excludePrivateIPs,
|
||||
.persistentKeepAlive,
|
||||
.deletePeer]
|
||||
]
|
||||
|
||||
// Scratchpad for entered data
|
||||
|
||||
class InterfaceData {
|
||||
var scratchpad: [InterfaceEditField: String] = [:]
|
||||
var fieldsWithError: Set<InterfaceEditField> = []
|
||||
var validatedConfiguration: InterfaceConfiguration? = nil
|
||||
subscript(field: InterfaceEditField) -> String {
|
||||
get {
|
||||
ensureScratchpadIsPrepared() // When starting to read a config, setup the scratchpad to serve as a cache
|
||||
return scratchpad[field] ?? ""
|
||||
}
|
||||
set(stringValue) {
|
||||
ensureScratchpadIsPrepared() // When starting to edit a config, setup the scratchpad
|
||||
validatedConfiguration = nil // The configuration will need to be revalidated
|
||||
if (stringValue.isEmpty) {
|
||||
scratchpad.removeValue(forKey: field)
|
||||
} else {
|
||||
scratchpad[field] = stringValue
|
||||
}
|
||||
}
|
||||
}
|
||||
func ensureScratchpadIsPrepared() {
|
||||
guard (scratchpad.isEmpty) else { return } // Already prepared
|
||||
guard let config = validatedConfiguration else { return } // Nothing to prepare it with
|
||||
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 validate() -> (success: Bool, errorMessage: String) {
|
||||
var firstErrorMessage: String? = nil
|
||||
func setErrorMessage(_ errorMessage: String) {
|
||||
if (firstErrorMessage == nil) {
|
||||
firstErrorMessage = errorMessage
|
||||
}
|
||||
}
|
||||
fieldsWithError.removeAll()
|
||||
guard let name = scratchpad[.name] else {
|
||||
fieldsWithError.insert(.name)
|
||||
return(false, "Interface name is required")
|
||||
}
|
||||
guard let privateKeyString = scratchpad[.privateKey] else {
|
||||
fieldsWithError.insert(.privateKey)
|
||||
return (false, "Interface's private key is required")
|
||||
}
|
||||
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else {
|
||||
fieldsWithError.insert(.privateKey)
|
||||
return(false, "Interface's private key should be a 32-byte key in base64 encoding")
|
||||
}
|
||||
var config = InterfaceConfiguration(name: name, privateKey: privateKey)
|
||||
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)
|
||||
setErrorMessage("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)
|
||||
setErrorMessage("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)
|
||||
setErrorMessage("Interface's MTU should be a number")
|
||||
}
|
||||
}
|
||||
// TODO: Validate DNS
|
||||
if let dnsString = scratchpad[.dns] {
|
||||
config.dns = dnsString
|
||||
}
|
||||
|
||||
if let firstErrorMessage = firstErrorMessage {
|
||||
return (false, firstErrorMessage)
|
||||
}
|
||||
validatedConfiguration = config
|
||||
return (true, "")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
ensureScratchpadIsPrepared() // When starting to read a config, setup the scratchpad to serve as a cache
|
||||
return scratchpad[field] ?? ""
|
||||
}
|
||||
set(stringValue) {
|
||||
ensureScratchpadIsPrepared() // When starting to edit a config, setup the scratchpad
|
||||
validatedConfiguration = nil // The configuration will need to be revalidated
|
||||
if (stringValue.isEmpty) {
|
||||
scratchpad.removeValue(forKey: field)
|
||||
} else {
|
||||
scratchpad[field] = stringValue
|
||||
}
|
||||
}
|
||||
}
|
||||
func ensureScratchpadIsPrepared() {
|
||||
guard (scratchpad.isEmpty) else { return }
|
||||
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 validate() -> (success: Bool, errorMessage: String) {
|
||||
var firstErrorMessage: String? = nil
|
||||
func setErrorMessage(_ errorMessage: String) {
|
||||
if (firstErrorMessage == nil) {
|
||||
firstErrorMessage = errorMessage
|
||||
}
|
||||
}
|
||||
fieldsWithError.removeAll()
|
||||
guard let publicKeyString = scratchpad[.publicKey] else {
|
||||
fieldsWithError.insert(.publicKey)
|
||||
return (success: false, errorMessage: "Peer's public key is required")
|
||||
}
|
||||
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else {
|
||||
fieldsWithError.insert(.publicKey)
|
||||
return (success: false, errorMessage: "Peer's public key should be a 32-byte key in base64 encoding")
|
||||
}
|
||||
var config = PeerConfiguration(publicKey: publicKey)
|
||||
if let preSharedKeyString = scratchpad[.publicKey] {
|
||||
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 {
|
||||
config.preSharedKey = preSharedKey
|
||||
} else {
|
||||
fieldsWithError.insert(.preSharedKey)
|
||||
setErrorMessage("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)
|
||||
setErrorMessage("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)
|
||||
setErrorMessage("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)
|
||||
setErrorMessage("Peer's persistent keepalive should be a number")
|
||||
}
|
||||
}
|
||||
|
||||
if let firstErrorMessage = firstErrorMessage {
|
||||
return (false, firstErrorMessage)
|
||||
}
|
||||
validatedConfiguration = config
|
||||
scratchpad = [:]
|
||||
return (true, "")
|
||||
}
|
||||
}
|
||||
|
||||
var interfaceData: InterfaceData
|
||||
var peersData: [PeerData]
|
||||
|
||||
// MARK: TunnelEditTableViewController methods
|
||||
let tunnelViewModel: TunnelViewModel
|
||||
|
||||
init() {
|
||||
interfaceData = InterfaceData()
|
||||
peersData = []
|
||||
tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil)
|
||||
super.init(style: .grouped)
|
||||
self.modalPresentationStyle = .formSheet
|
||||
}
|
||||
|
@ -299,7 +66,7 @@ extension TunnelEditTableViewController {
|
|||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
let numberOfPeers = tunnelViewModel.peersData.count
|
||||
|
||||
return numberOfInterfaceSections + (numberOfPeers * numberOfPeerSections) + 1
|
||||
}
|
||||
|
@ -307,7 +74,7 @@ extension TunnelEditTableViewController {
|
|||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
let numberOfPeers = tunnelViewModel.peersData.count
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
|
@ -325,7 +92,7 @@ extension TunnelEditTableViewController {
|
|||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
let numberOfPeers = tunnelViewModel.peersData.count
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
|
@ -343,13 +110,14 @@ extension TunnelEditTableViewController {
|
|||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
let numberOfPeers = tunnelViewModel.peersData.count
|
||||
|
||||
let section = indexPath.section
|
||||
let row = indexPath.row
|
||||
|
||||
if (section < numberOfInterfaceSections) {
|
||||
// Interface
|
||||
let interfaceData = tunnelViewModel.interfaceData
|
||||
let field = interfaceEditFieldsBySection[section][row]
|
||||
if (field == .generateKeyPair) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell
|
||||
|
@ -360,49 +128,18 @@ extension TunnelEditTableViewController {
|
|||
return cell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell
|
||||
// Set key
|
||||
cell.key = field.rawValue
|
||||
switch (field) {
|
||||
case .name:
|
||||
// Set placeholder text
|
||||
if (field == .name || field == .privateKey) {
|
||||
cell.placeholderText = "Required"
|
||||
cell.value = interfaceData[.name]
|
||||
cell.onValueChanged = { [weak interfaceData] value in
|
||||
interfaceData?[.name] = value
|
||||
}
|
||||
case .privateKey:
|
||||
cell.placeholderText = "Required"
|
||||
cell.value = interfaceData[.privateKey]
|
||||
cell.onValueChanged = { [weak interfaceData] value in
|
||||
interfaceData?[.privateKey] = value
|
||||
}
|
||||
case .publicKey:
|
||||
cell.isValueEditable = false
|
||||
cell.value = "Unimplemented"
|
||||
case .generateKeyPair:
|
||||
break
|
||||
case .addresses:
|
||||
cell.value = interfaceData[.addresses]
|
||||
cell.onValueChanged = { [weak interfaceData] value in
|
||||
interfaceData?[.addresses] = value
|
||||
}
|
||||
break
|
||||
case .listenPort:
|
||||
cell.value = interfaceData[.listenPort]
|
||||
cell.onValueChanged = { [weak interfaceData] value in
|
||||
interfaceData?[.listenPort] = value
|
||||
}
|
||||
break
|
||||
case .mtu:
|
||||
} else if (field == .mtu) {
|
||||
cell.placeholderText = "Automatic"
|
||||
cell.value = interfaceData[.mtu]
|
||||
cell.onValueChanged = { [weak interfaceData] value in
|
||||
interfaceData?[.mtu] = value
|
||||
}
|
||||
case .dns:
|
||||
cell.value = interfaceData[.dns]
|
||||
cell.onValueChanged = { [weak interfaceData] value in
|
||||
interfaceData?[.dns] = value
|
||||
}
|
||||
break
|
||||
}
|
||||
// Bind values to view model
|
||||
cell.value = interfaceData[field]
|
||||
cell.onValueChanged = { [weak interfaceData] value in
|
||||
interfaceData?[field] = value
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
@ -410,7 +147,7 @@ extension TunnelEditTableViewController {
|
|||
// Peer
|
||||
let peerIndex = Int((section - numberOfInterfaceSections) / numberOfPeerSections)
|
||||
let peerSectionIndex = (section - numberOfInterfaceSections) % numberOfPeerSections
|
||||
let peerData = peersData[peerIndex]
|
||||
let peerData = tunnelViewModel.peersData[peerIndex]
|
||||
let field = peerEditFieldsBySection[peerSectionIndex][row]
|
||||
if (field == .deletePeer) {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewButtonCell.id, for: indexPath) as! TunnelsEditTableViewButtonCell
|
||||
|
@ -433,42 +170,16 @@ extension TunnelEditTableViewController {
|
|||
return cell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsEditTableViewKeyValueCell.id, for: indexPath) as! TunnelsEditTableViewKeyValueCell
|
||||
// Set key
|
||||
cell.key = field.rawValue
|
||||
switch (field) {
|
||||
case .publicKey:
|
||||
// Set placeholder text
|
||||
if (field == .publicKey) {
|
||||
cell.placeholderText = "Required"
|
||||
cell.value = peerData[.publicKey]
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
peerData?[.publicKey] = value
|
||||
}
|
||||
case .preSharedKey:
|
||||
cell.value = peerData[.preSharedKey]
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
peerData?[.preSharedKey] = value
|
||||
}
|
||||
break
|
||||
case .endpoint:
|
||||
cell.value = peerData[.endpoint]
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
peerData?[.endpoint] = value
|
||||
}
|
||||
break
|
||||
case .persistentKeepAlive:
|
||||
cell.value = peerData[.persistentKeepAlive]
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
peerData?[.persistentKeepAlive] = value
|
||||
}
|
||||
break
|
||||
case .allowedIPs:
|
||||
cell.value = peerData[.allowedIPs]
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
peerData?[.allowedIPs] = value
|
||||
}
|
||||
break
|
||||
case .excludePrivateIPs:
|
||||
break
|
||||
case .deletePeer:
|
||||
break
|
||||
}
|
||||
// Bind values to view model
|
||||
cell.value = peerData[field]
|
||||
cell.onValueChanged = { [weak peerData] value in
|
||||
peerData?[field] = value
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
@ -489,29 +200,21 @@ extension TunnelEditTableViewController {
|
|||
func appendEmptyPeer() -> IndexSet {
|
||||
let numberOfInterfaceSections = interfaceEditFieldsBySection.count
|
||||
let numberOfPeerSections = peerEditFieldsBySection.count
|
||||
let numberOfPeers = peersData.count
|
||||
|
||||
let peer = PeerData(index: peersData.count)
|
||||
peersData.append(peer)
|
||||
tunnelViewModel.appendEmptyPeer()
|
||||
let addedPeerIndex = tunnelViewModel.peersData.count - 1
|
||||
|
||||
let firstAddedSectionIndex = (numberOfInterfaceSections + numberOfPeers * numberOfPeerSections)
|
||||
let firstAddedSectionIndex = (numberOfInterfaceSections + addedPeerIndex * numberOfPeerSections)
|
||||
let addedSectionIndices = IndexSet(integersIn: firstAddedSectionIndex ..< firstAddedSectionIndex + numberOfPeerSections)
|
||||
return addedSectionIndices
|
||||
}
|
||||
|
||||
func deletePeer(peer: PeerData) -> IndexSet {
|
||||
func deletePeer(peer: TunnelViewModel.PeerData) -> 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
|
||||
}
|
||||
assert(peer.index < tunnelViewModel.peersData.count)
|
||||
tunnelViewModel.deletePeer(peer: peer)
|
||||
|
||||
let firstRemovedSectionIndex = (numberOfInterfaceSections + peer.index * numberOfPeerSections)
|
||||
let removedSectionIndices = IndexSet(integersIn: firstRemovedSectionIndex ..< firstRemovedSectionIndex + numberOfPeerSections)
|
||||
|
|
Loading…
Reference in New Issue