diff --git a/ios/NebulaNetworkExtension/Keychain.swift b/ios/NebulaNetworkExtension/Keychain.swift index 0668987..8b4d020 100644 --- a/ios/NebulaNetworkExtension/Keychain.swift +++ b/ios/NebulaNetworkExtension/Keychain.swift @@ -5,31 +5,31 @@ let groupName = "group.net.defined.mobileNebula" class KeyChain { class func save(key: String, data: Data, managed: Bool) -> Bool { var query: [String: Any] = [ - kSecClass as String : kSecClassGenericPassword as String, - kSecAttrAccount as String : key, - kSecValueData as String : data, + kSecClass as String: kSecClassGenericPassword as String, + kSecAttrAccount as String: key, + kSecValueData as String: data, kSecAttrAccessGroup as String: groupName, ] - - if (managed) { + + if managed { query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock } // Attempt to delete an existing key to allow for an overwrite - _ = self.delete(key: key) + _ = delete(key: key) return SecItemAdd(query as CFDictionary, nil) == 0 } class func load(key: String) -> Data? { let query: [String: Any] = [ - kSecClass as String : kSecClassGenericPassword, - kSecAttrAccount as String : key, - kSecReturnData as String : kCFBooleanTrue!, - kSecMatchLimit as String : kSecMatchLimitOne, + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key, + kSecReturnData as String: kCFBooleanTrue!, + kSecMatchLimit as String: kSecMatchLimitOne, kSecAttrAccessGroup as String: groupName, ] - var dataTypeRef: AnyObject? = nil + var dataTypeRef: AnyObject? let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) @@ -39,31 +39,29 @@ class KeyChain { return nil } } - + class func delete(key: String) -> Bool { - let query: [String: Any] = [ - kSecClass as String : kSecClassGenericPassword as String, - kSecAttrAccount as String : key, - kSecAttrAccessGroup as String: groupName, - ] + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword as String, + kSecAttrAccount as String: key, + kSecAttrAccessGroup as String: groupName, + ] return SecItemDelete(query as CFDictionary) == 0 } } extension Data { - init(from value: T) { var value = value var data = Data() - withUnsafePointer(to: &value, { (ptr: UnsafePointer) -> Void in + withUnsafePointer(to: &value) { (ptr: UnsafePointer) in data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1)) - }) + } self.init(data) } - func to(type: T.Type) -> T { - return self.withUnsafeBytes { $0.load(as: T.self) } + func to(type _: T.Type) -> T { + return withUnsafeBytes { $0.load(as: T.self) } } } - diff --git a/ios/NebulaNetworkExtension/PacketTunnelProvider.swift b/ios/NebulaNetworkExtension/PacketTunnelProvider.swift index dffb054..887693d 100644 --- a/ios/NebulaNetworkExtension/PacketTunnelProvider.swift +++ b/ios/NebulaNetworkExtension/PacketTunnelProvider.swift @@ -1,5 +1,5 @@ -import NetworkExtension import MobileNebula +import NetworkExtension import os.log import SwiftyJSON @@ -17,66 +17,65 @@ enum AppMessageError: Error { extension AppMessageError: LocalizedError { public var description: String? { switch self { - case .unknownIPCType(let command): + case let .unknownIPCType(command): return NSLocalizedString("Unknown IPC message type \(String(command))", comment: "") } } } - class PacketTunnelProvider: NEPacketTunnelProvider { private var networkMonitor: NWPathMonitor? - + private var site: Site? private let log = Logger(subsystem: "net.defined.mobileNebula", category: "PacketTunnelProvider") private var nebula: MobileNebulaNebula? private var dnUpdater = DNUpdater() private var didSleep = false private var cachedRouteDescription: String? - - override func startTunnel(options: [String : NSObject]? = nil) async throws { + + override func startTunnel(options: [String: NSObject]? = nil) async throws { // There is currently no way to get initialization errors back to the UI via completionHandler here // `expectStart` is sent only via the UI which means we should wait for the real start command which has another completion handler the UI can intercept if options?["expectStart"] != nil { // startTunnel must complete before IPC will work return } - + // VPN is being booted out of band of the UI. Use the system completion handler as there will be nothing to route initialization errors to but we still need to report // success/fail by the presence of an error or nil try await start() } - + private func start() async throws { var manager: NETunnelProviderManager? var config: Data var key: String - + do { // Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS // TODO: Remove else once we drop support for iOS 16 if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) { - manager = try await self.findManager() + manager = try await findManager() guard let foundManager = manager else { throw VPNStartError.couldNotFindManager } - self.site = try Site(manager: foundManager) + site = try Site(manager: foundManager) } else { // This does not save the manager with the site, which means we cannot update the // vpn profile name when updates happen (rare). - self.site = try Site(proto: self.protocolConfiguration as! NETunnelProviderProtocol) + site = try Site(proto: protocolConfiguration as! NETunnelProviderProtocol) } - config = try self.site!.getConfig() + config = try site!.getConfig() } catch { - //TODO: need a way to notify the app - self.log.error("Failed to render config from vpn object") + // TODO: need a way to notify the app + log.error("Failed to render config from vpn object") throw error } - let _site = self.site! + let _site = site! key = try _site.getKey() - - guard let fileDescriptor = self.tunnelFileDescriptor else { + + guard let fileDescriptor = tunnelFileDescriptor else { throw VPNStartError.noTunFileDescriptor } let tunFD = Int(fileDescriptor) @@ -87,7 +86,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { // Make sure our ip is routed to the tun device var err: NSError? let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err) - if (err != nil) { + if err != nil { throw err! } tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR]) @@ -96,7 +95,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { // Add our unsafe routes try _site.unsafeRoutes.forEach { unsafeRoute in let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err) - if (err != nil) { + if err != nil { throw err! } routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)) @@ -105,43 +104,43 @@ class PacketTunnelProvider: NEPacketTunnelProvider { tunnelNetworkSettings.ipv4Settings!.includedRoutes = routes tunnelNetworkSettings.mtu = _site.mtu as NSNumber - try await self.setTunnelNetworkSettings(tunnelNetworkSettings) + try await setTunnelNetworkSettings(tunnelNetworkSettings) var nebulaErr: NSError? - self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr) - self.startNetworkMonitor() + nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, site!.logFile, tunFD, &nebulaErr) + startNetworkMonitor() if nebulaErr != nil { - self.log.error("We had an error starting up: \(nebulaErr, privacy: .public)") + log.error("We had an error starting up: \(nebulaErr, privacy: .public)") throw nebulaErr! } - - self.nebula!.start() - self.dnUpdater.updateSingleLoop(site: self.site!, onUpdate: self.handleDNUpdate) + + nebula!.start() + dnUpdater.updateSingleLoop(site: site!, onUpdate: handleDNUpdate) } - + private func handleDNUpdate(newSite: Site) { do { - self.site = newSite - try self.nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey()) - + site = newSite + try nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey()) + } catch { log.error("Got an error while updating nebula \(error.localizedDescription, privacy: .public)") } } - -//TODO: Sleep/wake get called aggressively and do nothing to help us here, we should locate why that is and make these work appropriately + + // TODO: Sleep/wake get called aggressively and do nothing to help us here, we should locate why that is and make these work appropriately // override func sleep(completionHandler: @escaping () -> Void) { // nebula!.sleep() // completionHandler() // } - + private func findManager() async throws -> NETunnelProviderManager { - let targetProtoConfig = self.protocolConfiguration as? NETunnelProviderProtocol + let targetProtoConfig = protocolConfiguration as? NETunnelProviderProtocol guard let targetProviderConfig = targetProtoConfig?.providerConfiguration else { throw VPNStartError.noProviderConfig } let targetID = targetProviderConfig["id"] as? String - + // Load vpn configs from system, and find the manager matching the one being started let managers = try await NETunnelProviderManager.loadAllFromPreferences() for manager in managers { @@ -150,32 +149,32 @@ class PacketTunnelProvider: NEPacketTunnelProvider { throw VPNStartError.noProviderConfig } let id = mgrProviderConfig["id"] as? String - if (id == targetID) { + if id == targetID { return manager } } - + // If we didn't find anything, throw an error throw VPNStartError.noManagers } - + private func startNetworkMonitor() { networkMonitor = NWPathMonitor() - networkMonitor!.pathUpdateHandler = self.pathUpdate + networkMonitor!.pathUpdateHandler = pathUpdate networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor")) } - + private func stopNetworkMonitor() { - self.networkMonitor?.cancel() + networkMonitor?.cancel() networkMonitor = nil } - - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + + override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) { nebula?.stop() stopNetworkMonitor() completionHandler() } - + private func pathUpdate(path: Network.NWPath) { let routeDescription = collectAddresses(endpoints: path.gateways) if routeDescription != cachedRouteDescription { @@ -186,94 +185,93 @@ class PacketTunnelProvider: NEPacketTunnelProvider { cachedRouteDescription = routeDescription } } - + private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String { var str: [String] = [] - endpoints.forEach{ endpoint in + for endpoint in endpoints { switch endpoint { case let .hostPort(.ipv6(host), port): str.append("[\(host)]:\(port)") case let .hostPort(.ipv4(host), port): str.append("\(host):\(port)") default: - return + continue } } - + return str.sorted().joined(separator: ", ") } - + override func handleAppMessage(_ data: Data) async -> Data? { guard let call = try? JSONDecoder().decode(IPCRequest.self, from: data) else { log.error("Failed to decode IPCRequest from network extension") return nil } - + var error: Error? var data: JSON? - + // start command has special treatment due to needing to call two completers if call.command == "start" { do { - try await self.start() + try await start() // No response data, this is expected on a clean start - return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil)) + return try? JSONEncoder().encode(IPCResponse(type: .success, message: nil)) } catch { defer { self.cancelTunnelWithError(error) } - return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error.localizedDescription))) + return try? JSONEncoder().encode(IPCResponse(type: .error, message: JSON(error.localizedDescription))) } } - + if nebula == nil { // Respond with an empty success message in the event a command comes in before we've truly started log.warning("Received command but do not have a nebula instance") - return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil)) + return try? JSONEncoder().encode(IPCResponse(type: .success, message: nil)) } - - //TODO: try catch over all this + + // TODO: try catch over all this switch call.command { case "listHostmap": (data, error) = listHostmap(pending: false) case "listPendingHostmap": (data, error) = listHostmap(pending: true) case "getHostInfo": (data, error) = getHostInfo(args: call.arguments!) case "setRemoteForTunnel": (data, error) = setRemoteForTunnel(args: call.arguments!) case "closeTunnel": (data, error) = closeTunnel(args: call.arguments!) - default: error = AppMessageError.unknownIPCType(command: call.command) } - - if (error != nil) { - return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error"))) + + if error != nil { + return try? JSONEncoder().encode(IPCResponse(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error"))) } else { - return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data)) + return try? JSONEncoder().encode(IPCResponse(type: .success, message: data)) } } - + private func listHostmap(pending: Bool) -> (JSON?, Error?) { var err: NSError? let res = nebula!.listHostmap(pending, error: &err) return (JSON(res), err) } - + private func getHostInfo(args: JSON) -> (JSON?, Error?) { var err: NSError? let res = nebula!.getHostInfo(byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err) return (JSON(res), err) } - + private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) { var err: NSError? let res = nebula!.setRemoteForTunnel(args["vpnIp"].string, addr: args["addr"].string, error: &err) return (JSON(res), err) } - + private func closeTunnel(args: JSON) -> (JSON?, Error?) { let res = nebula!.closeTunnel(args["vpnIp"].string) return (JSON(res), nil) } - + private var tunnelFileDescriptor: Int32? { var ctlInfo = ctl_info() withUnsafeMutablePointer(to: &ctlInfo.ctl_name) { @@ -281,7 +279,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { _ = strcpy($0, "com.apple.net.utun_control") } } - for fd: Int32 in 0...1024 { + for fd: Int32 in 0 ... 1024 { var addr = sockaddr_ctl() var ret: Int32 = -1 var len = socklen_t(MemoryLayout.size(ofValue: addr)) @@ -306,4 +304,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return nil } } - diff --git a/ios/NebulaNetworkExtension/Site.swift b/ios/NebulaNetworkExtension/Site.swift index 3523bdb..a4ac9fa 100644 --- a/ios/NebulaNetworkExtension/Site.swift +++ b/ios/NebulaNetworkExtension/Site.swift @@ -1,12 +1,12 @@ -import NetworkExtension import MobileNebula -import SwiftyJSON +import NetworkExtension import os.log +import SwiftyJSON let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site") enum SiteError: Error { - case nonConforming(site: [String : Any]?) + case nonConforming(site: [String: Any]?) case noCertificate case keyLoad case keySave @@ -21,7 +21,7 @@ enum SiteError: Error { extension SiteError: CustomStringConvertible { public var description: String { switch self { - case .nonConforming(let site): + case let .nonConforming(site): return String("Non-conforming site \(String(describing: site))") case .noCertificate: return "No certificate found" @@ -35,20 +35,20 @@ extension SiteError: CustomStringConvertible { return "failed to find dn credentials in keychain" case .dnCredentialSave: return "failed to store dn credentials in keychain" - case .unexpected(_): + case .unexpected: return "An unexpected error occurred." } } } enum IPCResponseType: String, Codable { - case error = "error" - case success = "success" + case error + case success } class IPCResponse: Codable { var type: IPCResponseType - //TODO: change message to data? + // TODO: change message to data? var message: JSON? init(type: IPCResponseType, message: JSON?) { @@ -131,7 +131,7 @@ struct CertificateValidity: Codable { } } -let statusMap: Dictionary = [ +let statusMap: [NEVPNStatus: Bool] = [ NEVPNStatus.invalid: false, NEVPNStatus.disconnected: false, NEVPNStatus.connecting: false, @@ -140,7 +140,7 @@ let statusMap: Dictionary = [ NEVPNStatus.disconnecting: true, ] -let statusString: Dictionary = [ +let statusString: [NEVPNStatus: String] = [ NEVPNStatus.invalid: "Invalid configuration", NEVPNStatus.disconnected: "Disconnected", NEVPNStatus.connecting: "Connecting...", @@ -156,7 +156,7 @@ class Site: Codable { var id: String // Stored in proto - var staticHostmap: Dictionary + var staticHostmap: [String: StaticHosts] var unsafeRoutes: [UnsafeRoute] var cert: CertificateInfo? var ca: [CertificateInfo] @@ -166,7 +166,7 @@ class Site: Codable { var cipher: String var sortKey: Int var logVerbosity: String - var connected: Bool? //TODO: active is a better name + var connected: Bool? // TODO: active is a better name var status: String? var logFile: String? var managed: Bool @@ -186,12 +186,12 @@ class Site: Codable { /// Creates a new site from a vpn manager instance. Mainly used by the UI. A manager is required to be able to edit the system profile convenience init(manager: NETunnelProviderManager) throws { - //TODO: Throw an error and have Sites delete the site, notify the user instead of using ! + // TODO: Throw an error and have Sites delete the site, notify the user instead of using ! let proto = manager.protocolConfiguration as! NETunnelProviderProtocol try self.init(proto: proto) self.manager = manager - self.connected = statusMap[manager.connection.status] - self.status = statusString[manager.connection.status] + connected = statusMap[manager.connection.status] + status = statusString[manager.connection.status] } convenience init(proto: NETunnelProviderProtocol) throws { @@ -202,7 +202,7 @@ class Site: Codable { let decoder = JSONDecoder() let incoming = try decoder.decode(IncomingSite.self, from: config) self.init(incoming: incoming) - self.needsToMigrateToFS = true + needsToMigrateToFS = true return } @@ -244,18 +244,18 @@ class Site: Codable { do { let rawCert = incoming.cert let rawDetails = MobileNebulaParseCerts(rawCert, &err) - if (err != nil) { + if err != nil { throw err! } var certs: [CertificateInfo] certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!) - if (certs.count == 0) { + if certs.count == 0 { throw SiteError.noCertificate } cert = certs[0] - if (!cert!.validity.valid) { + if !cert!.validity.valid { errors.append("Certificate is invalid: \(cert!.validity.reason)") } @@ -266,19 +266,19 @@ class Site: Codable { do { let rawCa = incoming.ca let rawCaDetails = MobileNebulaParseCerts(rawCa, &err) - if (err != nil) { + if err != nil { throw err! } ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!) var hasErrors = false - ca.forEach { cert in - if (!cert.validity.valid) { + for cert in ca { + if !cert.validity.valid { hasErrors = true } } - if (hasErrors && !managed) { + if hasErrors && !managed { errors.append("There are issues with 1 or more ca certificates") } @@ -288,17 +288,17 @@ class Site: Codable { } do { - logFile = try SiteList.getSiteLogFile(id: self.id, createDir: true).path + logFile = try SiteList.getSiteLogFile(id: id, createDir: true).path } catch { logFile = nil errors.append("Unable to create the site directory: \(error.localizedDescription)") } - if (managed && (try? getDNCredentials())?.invalid != false) { + if managed && (try? getDNCredentials())?.invalid != false { errors.append("Unable to fetch managed updates - please re-enroll the device") } - if (errors.isEmpty) { + if errors.isEmpty { do { let encoder = JSONEncoder() let rawConfig = try encoder.encode(incoming) @@ -307,7 +307,7 @@ class Site: Codable { var err: NSError? MobileNebulaTestConfig(strConfig, key, &err) - if (err != nil) { + if err != nil { throw err! } } catch { @@ -322,12 +322,12 @@ class Site: Codable { throw SiteError.keyLoad } - //TODO: make sure this is valid on return! + // TODO: make sure this is valid on return! return String(decoding: keyData, as: UTF8.self) } func getDNCredentials() throws -> DNCredentials { - if (!managed) { + if !managed { throw SiteError.unmanagedGetCredentials } @@ -344,7 +344,7 @@ class Site: Codable { let creds = try getDNCredentials() creds.invalid = true - if (!(try creds.save(siteID: self.id))) { + if try !(creds.save(siteID: id)) { throw SiteError.dnCredentialLoad } } @@ -353,13 +353,13 @@ class Site: Codable { let creds = try getDNCredentials() creds.invalid = false - if (!(try creds.save(siteID: self.id))) { + if try !(creds.save(siteID: id)) { throw SiteError.dnCredentialSave } } func getConfig() throws -> Data { - return try self.incomingSite!.getConfig() + return try incomingSite!.getConfig() } // Limits what we export to the UI @@ -429,7 +429,7 @@ class DNCredentials: Codable { struct IncomingSite: Codable { var name: String var id: String - var staticHostmap: Dictionary + var staticHostmap: [String: StaticHosts] var unsafeRoutes: [UnsafeRoute]? var cert: String? var ca: String? @@ -456,11 +456,11 @@ struct IncomingSite: Codable { return try encoder.encode(config) } - func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> ()) { + func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> Void) { let configPath: URL do { - configPath = try SiteList.getSiteConfigFile(id: self.id, createDir: true) + configPath = try SiteList.getSiteConfigFile(id: id, createDir: true) } catch { callback(error) @@ -469,45 +469,44 @@ struct IncomingSite: Codable { log.notice("Saving to \(configPath, privacy: .public)") do { - if (self.key != nil) { - let data = self.key!.data(using: .utf8) - if (!KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false)) { + if key != nil { + let data = key!.data(using: .utf8) + if !KeyChain.save(key: "\(id).key", data: data!, managed: managed ?? false) { return callback(SiteError.keySave) } } do { - if ((try self.dnCredentials?.save(siteID: self.id)) == false) { + if try (dnCredentials?.save(siteID: id)) == false { return callback(SiteError.dnCredentialSave) } } catch { return callback(error) } - try self.getConfig().write(to: configPath) + try getConfig().write(to: configPath) } catch { return callback(error) } - -#if targetEnvironment(simulator) - // We are on a simulator and there is no NEVPNManager for us to interact with - callback(nil) -#else - if saveToManager { - self.saveToManager(manager: manager, callback: callback) - } else { + #if targetEnvironment(simulator) + // We are on a simulator and there is no NEVPNManager for us to interact with callback(nil) - } -#endif + #else + if saveToManager { + self.saveToManager(manager: manager, callback: callback) + } else { + callback(nil) + } + #endif } - private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) { - if (manager != nil) { + private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> Void) { + if manager != nil { // We need to refresh our settings to properly update config manager?.loadFromPreferences { error in - if (error != nil) { + if error != nil { return callback(error) } @@ -519,25 +518,25 @@ struct IncomingSite: Codable { return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback) } - private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> ()) { + private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> Void) { // Stuff our details in the protocol let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() - proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension"; + proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension" // WARN: If we stop setting providerConfiguration["id"] here, we'll need to use something else to match // managers in PacketTunnelProvider.findManager - proto.providerConfiguration = ["id": self.id] + proto.providerConfiguration = ["id": id] proto.serverAddress = "Nebula" // Finish up the manager, this is what stores everything at the system level manager.protocolConfiguration = proto - //TODO: cert name? manager.protocolConfiguration?.username + // TODO: cert name? manager.protocolConfiguration?.username - //TODO: This is what is shown on the vpn page. We should add more identifying details in - manager.localizedDescription = self.name + // TODO: This is what is shown on the vpn page. We should add more identifying details in + manager.localizedDescription = name manager.isEnabled = true - manager.saveToPreferences{ error in - return callback(error) + manager.saveToPreferences { error in + callback(error) } } } diff --git a/ios/NebulaNetworkExtension/SiteList.swift b/ios/NebulaNetworkExtension/SiteList.swift index b6f2bb9..ec2de4d 100644 --- a/ios/NebulaNetworkExtension/SiteList.swift +++ b/ios/NebulaNetworkExtension/SiteList.swift @@ -2,107 +2,107 @@ import NetworkExtension class SiteList { private var sites = [String: Site]() - + /// Gets the root directory that can be used to share files between the UI and VPN process. Does ensure the directory exists static func getRootDir() throws -> URL { let fileManager = FileManager.default let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")! - - if (!fileManager.fileExists(atPath: rootDir.absoluteString)) { + + if !fileManager.fileExists(atPath: rootDir.absoluteString) { try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true) } - + return rootDir } - + /// Gets the directory where all sites live, $rootDir/sites. Does ensure the directory exists static func getSitesDir() throws -> URL { let fileManager = FileManager.default let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true) - if (!fileManager.fileExists(atPath: sitesDir.absoluteString)) { + if !fileManager.fileExists(atPath: sitesDir.absoluteString) { try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true) } return sitesDir } - + /// Gets the directory where a single site would live, $rootDir/sites/$siteID static func getSiteDir(id: String, create: Bool = false) throws -> URL { let fileManager = FileManager.default let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true) - if (create && !fileManager.fileExists(atPath: siteDir.absoluteString)) { + if create && !fileManager.fileExists(atPath: siteDir.absoluteString) { try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true) } return siteDir } - + /// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL { return try getSiteDir(id: id, create: createDir).appendingPathComponent("config", isDirectory: false).appendingPathExtension("json") } - + /// Gets the file that represents the site log output, $rootDir/sites/$siteID/log static func getSiteLogFile(id: String, createDir: Bool) throws -> URL { return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false) } - - init(completion: @escaping ([String: Site]?, Error?) -> ()) { -#if targetEnvironment(simulator) - SiteList.loadAllFromFS { sites, err in - if sites != nil { - self.sites = sites! + + init(completion: @escaping ([String: Site]?, Error?) -> Void) { + #if targetEnvironment(simulator) + SiteList.loadAllFromFS { sites, err in + if sites != nil { + self.sites = sites! + } + completion(sites, err) } - completion(sites, err) - } -#else - SiteList.loadAllFromNETPM { sites, err in - if sites != nil { - self.sites = sites! + #else + SiteList.loadAllFromNETPM { sites, err in + if sites != nil { + self.sites = sites! + } + completion(sites, err) } - completion(sites, err) - } -#endif + #endif } - - private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> ()) { + + private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> Void) { let fileManager = FileManager.default var siteDirs: [URL] var sites = [String: Site]() - + do { siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil) - + } catch { completion(nil, error) return } - - siteDirs.forEach { path in + + for path in siteDirs { do { let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json")) sites[site.id] = site - + } catch { print(error) try? fileManager.removeItem(at: path) print("Deleted non conforming site \(path)") } } - + completion(sites, nil) } - - private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> ()) { + + private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> Void) { var sites = [String: Site]() - + // dispatchGroup is used to ensure we have migrated all sites before returning them // If there are no sites to migrate, there are never any entrants let dispatchGroup = DispatchGroup() - - NETunnelProviderManager.loadAllFromPreferences() { newManagers, err in - if (err != nil) { + + NETunnelProviderManager.loadAllFromPreferences { newManagers, err in + if err != nil { return completion(nil, err) } - + newManagers?.forEach { manager in do { let site = try Site(manager: manager) @@ -112,28 +112,28 @@ class SiteList { if error != nil { print("Error while migrating site to fs: \(error!.localizedDescription)") } - + print("Migrated site to fs: \(site.name)") site.needsToMigrateToFS = false dispatchGroup.leave() } } sites[site.id] = site - + } catch { - //TODO: notify the user about this + // TODO: notify the user about this print("Deleted non conforming site \(manager) \(error)") manager.removeFromPreferences() - //TODO: delete from disk, we need to try and discover the site id though + // TODO: delete from disk, we need to try and discover the site id though } } - + dispatchGroup.notify(queue: .main) { completion(sites, nil) } } } - + func getSites() -> [String: Site] { return sites } diff --git a/ios/Runner/APIClient.swift b/ios/Runner/APIClient.swift index b8f2ef1..092d940 100644 --- a/ios/Runner/APIClient.swift +++ b/ios/Runner/APIClient.swift @@ -7,17 +7,17 @@ enum APIClientError: Error { class APIClient { let apiClient: MobileNebulaAPIClient let json = JSONDecoder() - + init() { let packageInfo = PackageInfo() apiClient = MobileNebulaNewAPIClient("MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")! } - + func enroll(code: String) throws -> IncomingSite { let res = try apiClient.enroll(code) return try decodeIncomingSite(jsonSite: res.site) } - + func tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String) throws -> IncomingSite? { let res: MobileNebulaTryUpdateResult do { @@ -26,23 +26,24 @@ class APIClient { hostID: hostID, privateKey: privateKey, counter: counter, - trustedKeys: trustedKeys) + trustedKeys: trustedKeys + ) } catch { // type information from Go is not available, use string matching instead - if (error.localizedDescription == "invalid credentials") { + if error.localizedDescription == "invalid credentials" { throw APIClientError.invalidCredentials } - + throw error } - - if (res.fetchedUpdate) { + + if res.fetchedUpdate { return try decodeIncomingSite(jsonSite: res.site) } - + return nil } - + private func decodeIncomingSite(jsonSite: String) throws -> IncomingSite { do { return try json.decode(IncomingSite.self, from: jsonSite.data(using: .utf8)!) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 8a44693..f8e4c14 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,8 +1,8 @@ -import UIKit import Flutter import MobileNebula import NetworkExtension import SwiftyJSON +import UIKit enum ChannelName { static let vpn = "net.defined.mobileNebula/NebulaVpnService" @@ -18,136 +18,130 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { private let apiClient = APIClient() private var sites: Sites? private var ui: FlutterMethodChannel? - - + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) - - + dnUpdater.updateAllLoop { site in // Signal the site has changed in case the current site details screen is active let container = self.sites?.getContainer(id: site.id) - if (container != nil) { + if container != nil { // Update references to the site with the new site config container!.site = site container!.updater.update(connected: site.connected ?? false, replaceSite: site) } - + // Signal to the main screen to reload self.ui?.invokeMethod("refreshSites", arguments: nil) } - + guard let controller = window?.rootViewController as? FlutterViewController else { fatalError("rootViewController is not type FlutterViewController") } - + sites = Sites(messenger: controller.binaryMessenger) ui = FlutterMethodChannel(name: ChannelName.vpn, binaryMessenger: controller.binaryMessenger) - - ui!.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + + ui!.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in switch call.method { case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result) case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result) case "nebula.renderConfig": return self.nebulaRenderConfig(call: call, result: result) case "nebula.verifyCertAndKey": return self.nebulaVerifyCertAndKey(call: call, result: result) - case "dn.enroll": return self.dnEnroll(call: call, result: result) - case "listSites": return self.listSites(result: result) case "deleteSite": return self.deleteSite(call: call, result: result) case "saveSite": return self.saveSite(call: call, result: result) case "startSite": return self.startSite(call: call, result: result) case "stopSite": return self.stopSite(call: call, result: result) - case "active.listHostmap": self.vpnRequest(command: "listHostmap", arguments: call.arguments, result: result) case "active.listPendingHostmap": self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result) case "active.getHostInfo": self.vpnRequest(command: "getHostInfo", arguments: call.arguments, result: result) case "active.setRemoteForTunnel": self.vpnRequest(command: "setRemoteForTunnel", arguments: call.arguments, result: result) case "active.closeTunnel": self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result) - default: result(FlutterMethodNotImplemented) } - }) - + } + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - + func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) { - guard let args = call.arguments as? Dictionary else { return result(NoArgumentsError()) } + guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) } guard let certs = args["certs"] else { return result(MissingArgumentError(message: "certs is a required argument")) } - + var err: NSError? let json = MobileNebulaParseCerts(certs, &err) - if (err != nil) { + if err != nil { return result(CallFailedError(message: "Error while parsing certificate(s)", details: err!.localizedDescription)) } - + return result(json) } - + func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) { - guard let args = call.arguments as? Dictionary else { return result(NoArgumentsError()) } + guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) } guard let cert = args["cert"] else { return result(MissingArgumentError(message: "cert is a required argument")) } guard let key = args["key"] else { return result(MissingArgumentError(message: "key is a required argument")) } - + var err: NSError? var validd: ObjCBool = false let valid = MobileNebulaVerifyCertAndKey(cert, key, &validd, &err) - if (err != nil) { + if err != nil { return result(CallFailedError(message: "Error while verifying certificate and private key", details: err!.localizedDescription)) } - + return result(valid) } - + func nebulaGenerateKeyPair(result: FlutterResult) { var err: NSError? let kp = MobileNebulaGenerateKeyPair(&err) - if (err != nil) { + if err != nil { return result(CallFailedError(message: "Error while generating key pairs", details: err!.localizedDescription)) } - + return result(kp) } - + func nebulaRenderConfig(call: FlutterMethodCall, result: FlutterResult) { guard let config = call.arguments as? String else { return result(NoArgumentsError()) } - + var err: NSError? let yaml = MobileNebulaRenderConfig(config, "", &err) - if (err != nil) { + if err != nil { return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription)) } - + return result(yaml) } - + func dnEnroll(call: FlutterMethodCall, result: @escaping FlutterResult) { guard let code = call.arguments as? String else { return result(NoArgumentsError()) } - + do { let site = try apiClient.enroll(code: code) - - let oldSite = self.sites?.getSite(id: site.id) + + let oldSite = sites?.getSite(id: site.id) site.save(manager: oldSite?.manager) { error in - if (error != nil) { + if error != nil { return result(CallFailedError(message: "Failed to enroll", details: error!.localizedDescription)) } - + result(nil) } } catch { return result(CallFailedError(message: "Error from DN api", details: error.localizedDescription)) } } - + func listSites(result: @escaping FlutterResult) { - self.sites?.loadSites { (sites, err) -> () in - if (err != nil) { + sites?.loadSites { sites, err in + if err != nil { return result(CallFailedError(message: "Failed to load site list", details: err!.localizedDescription)) } @@ -157,30 +151,30 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { result(ret) } } - + func deleteSite(call: FlutterMethodCall, result: @escaping FlutterResult) { guard let id = call.arguments as? String else { return result(NoArgumentsError()) } - //TODO: stop the site if its running currently - self.sites?.deleteSite(id: id) { error in - if (error != nil) { + // TODO: stop the site if its running currently + sites?.deleteSite(id: id) { error in + if error != nil { result(CallFailedError(message: "Failed to delete site", details: error!.localizedDescription)) } - + result(nil) } } - + func saveSite(call: FlutterMethodCall, result: @escaping FlutterResult) { guard let json = call.arguments as? String else { return result(NoArgumentsError()) } guard let data = json.data(using: .utf8) else { return result(NoArgumentsError()) } - + guard let site = try? JSONDecoder().decode(IncomingSite.self, from: data) else { return result(NoArgumentsError()) } - - let oldSite = self.sites?.getSite(id: site.id) + + let oldSite = sites?.getSite(id: site.id) site.save(manager: oldSite?.manager) { error in - if (error != nil) { + if error != nil { return result(CallFailedError(message: "Failed to save site", details: error!.localizedDescription)) } @@ -189,68 +183,68 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { } } } - - func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let args = call.arguments as? Dictionary else { return result(NoArgumentsError()) } - guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) } - -#if targetEnvironment(simulator) - let updater = self.sites?.getUpdater(id: id) - updater?.update(connected: true) -#else - let container = self.sites?.getContainer(id: id) - let manager = container?.site.manager - manager?.loadFromPreferences{ error in - //TODO: Handle load error - // This is silly but we need to enable the site each time to avoid situations where folks have multiple sites - manager?.isEnabled = true - manager?.saveToPreferences{ error in - //TODO: Handle load error - manager?.loadFromPreferences{ error in - //TODO: Handle load error - do { - container?.updater.startFunc = {() -> Void in - return self.vpnRequest(command: "start", arguments: args, result: result) + func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) } + guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) } + + #if targetEnvironment(simulator) + let updater = sites?.getUpdater(id: id) + updater?.update(connected: true) + #else + let container = sites?.getContainer(id: id) + let manager = container?.site.manager + + manager?.loadFromPreferences { error in + // TODO: Handle load error + // This is silly but we need to enable the site each time to avoid situations where folks have multiple sites + manager?.isEnabled = true + manager?.saveToPreferences { error in + // TODO: Handle load error + manager?.loadFromPreferences { error in + // TODO: Handle load error + do { + container?.updater.startFunc = { () in + self.vpnRequest(command: "start", arguments: args, result: result) + } + try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)]) + } catch { + return result(CallFailedError(message: "Could not start site", details: error.localizedDescription)) } - try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)]) - } catch { - return result(CallFailedError(message: "Could not start site", details: error.localizedDescription)) } } } - } -#endif + #endif } - + func stopSite(call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let args = call.arguments as? Dictionary else { return result(NoArgumentsError()) } + guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) } guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) } -#if targetEnvironment(simulator) - let updater = self.sites?.getUpdater(id: id) - updater?.update(connected: false) - -#else - let manager = self.sites?.getSite(id: id)?.manager - manager?.loadFromPreferences{ error in - //TODO: Handle load error - - manager?.connection.stopVPNTunnel() - return result(nil) - } -#endif + #if targetEnvironment(simulator) + let updater = sites?.getUpdater(id: id) + updater?.update(connected: false) + + #else + let manager = sites?.getSite(id: id)?.manager + manager?.loadFromPreferences { _ in + // TODO: Handle load error + + manager?.connection.stopVPNTunnel() + return result(nil) + } + #endif } - + func vpnRequest(command: String, arguments: Any?, result: @escaping FlutterResult) { - guard let args = arguments as? Dictionary else { return result(NoArgumentsError()) } + guard let args = arguments as? [String: Any] else { return result(NoArgumentsError()) } guard let id = args["id"] as? String else { return result(MissingArgumentError(message: "id is a required argument")) } let container = sites?.getContainer(id: id) - + if container == nil { // No site for this id return result(nil) } - + if !(container!.site.connected ?? false) { // Site isn't connected, no point in sending a command return result(nil) @@ -258,27 +252,27 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { if let session = container!.site.manager?.connection as? NETunnelProviderSession { do { - try session.sendProviderMessage(try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))) { data in + try session.sendProviderMessage(JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))) { data in if data == nil { return result(nil) } - - //print(String(decoding: data!, as: UTF8.self)) + + // print(String(decoding: data!, as: UTF8.self)) guard let res = try? JSONDecoder().decode(IPCResponse.self, from: data!) else { return result(CallFailedError(message: "Failed to decode response")) } - + if res.type == .success { return result(res.message?.object) } - + return result(CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error")) } } catch { return result(CallFailedError(message: error.localizedDescription)) } } else { - //TODO: we have a site without a manager, things have gone weird. How to handle since this shouldn't happen? + // TODO: we have a site without a manager, things have gone weird. How to handle since this shouldn't happen? result(nil) } } diff --git a/ios/Runner/DNUpdate.swift b/ios/Runner/DNUpdate.swift index fd566ea..571874e 100644 --- a/ios/Runner/DNUpdate.swift +++ b/ios/Runner/DNUpdate.swift @@ -5,14 +5,14 @@ class DNUpdater { private let apiClient = APIClient() private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater") - - func updateAll(onUpdate: @escaping (Site) -> ()) { - _ = SiteList{ (sites, _) -> () in + + func updateAll(onUpdate: @escaping (Site) -> Void) { + _ = SiteList { sites, _ in // NEVPN seems to force us onto the main thread and we are about to make network calls that // could block for a while. Push ourselves onto another thread to avoid blocking the UI. Task.detached(priority: .userInitiated) { sites?.values.forEach { site in - if (site.connected == true) { + if site.connected == true { // The vpn service is in charge of updating the currently connected site return } @@ -22,29 +22,29 @@ class DNUpdater { } } } - - func updateAllLoop(onUpdate: @escaping (Site) -> ()) { + + func updateAllLoop(onUpdate: @escaping (Site) -> Void) { timer.eventHandler = { self.updateAll(onUpdate: onUpdate) } timer.resume() } - - func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> ()) { + + func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> Void) { timer.eventHandler = { self.updateSite(site: site, onUpdate: onUpdate) } timer.resume() } - - func updateSite(site: Site, onUpdate: @escaping (Site) -> ()) { + + func updateSite(site: Site, onUpdate: @escaping (Site) -> Void) { do { - if (!site.managed) { + if !site.managed { return } - + let credentials = try site.getDNCredentials() - + let newSite: IncomingSite? do { newSite = try apiClient.tryUpdate( @@ -55,31 +55,31 @@ class DNUpdater { trustedKeys: credentials.trustedKeys ) } catch (APIClientError.invalidCredentials) { - if (!credentials.invalid) { + if !credentials.invalid { try site.invalidateDNCredentials() log.notice("Invalidated credentials in site: \(site.name, privacy: .public)") } - + return } - + let siteManager = site.manager let shouldSaveToManager = siteManager != nil || ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) - + newSite?.save(manager: site.manager, saveToManager: shouldSaveToManager) { error in - if (error != nil) { + if error != nil { self.log.error("failed to save update: \(error!.localizedDescription, privacy: .public)") } - + // reload nebula even if we couldn't save the vpn profile onUpdate(Site(incoming: newSite!)) } - - if (credentials.invalid) { + + if credentials.invalid { try site.validateDNCredentials() log.notice("Revalidated credentials in site \(site.name, privacy: .public)") } - + } catch { log.error("Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)") } @@ -88,7 +88,6 @@ class DNUpdater { // From https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9 class RepeatingTimer { - let timeInterval: TimeInterval init(timeInterval: TimeInterval) { diff --git a/ios/Runner/PackageInfo.swift b/ios/Runner/PackageInfo.swift index 50bba99..f401a50 100644 --- a/ios/Runner/PackageInfo.swift +++ b/ios/Runner/PackageInfo.swift @@ -5,11 +5,11 @@ class PackageInfo { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - - if (buildNumber == nil) { + + if buildNumber == nil { return version } - + return "\(version)-\(buildNumber!)" } @@ -18,7 +18,7 @@ class PackageInfo { Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "Nebula" } - + func getSystemVersion() -> String { let osVersion = ProcessInfo.processInfo.operatingSystemVersion return "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" diff --git a/ios/Runner/Sites.swift b/ios/Runner/Sites.swift index 7a8f79f..9b97d4c 100644 --- a/ios/Runner/Sites.swift +++ b/ios/Runner/Sites.swift @@ -1,10 +1,10 @@ -import NetworkExtension import MobileNebula +import NetworkExtension class SiteContainer { var site: Site var updater: SiteUpdater - + init(site: Site, updater: SiteUpdater) { self.site = site self.updater = updater @@ -14,39 +14,39 @@ class SiteContainer { class Sites { private var containers = [String: SiteContainer]() private var messenger: FlutterBinaryMessenger? - + init(messenger: FlutterBinaryMessenger?) { self.messenger = messenger } - func loadSites(completion: @escaping ([String: Site]?, Error?) -> ()) { - _ = SiteList { (sites, err) in - if (err != nil) { + func loadSites(completion: @escaping ([String: Site]?, Error?) -> Void) { + _ = SiteList { sites, err in + if err != nil { return completion(nil, err) } - - sites?.values.forEach{ site in + + sites?.values.forEach { site in var updater = self.containers[site.id]?.updater - if (updater != nil) { + if updater != nil { updater!.setSite(site: site) } else { updater = SiteUpdater(messenger: self.messenger!, site: site) } self.containers[site.id] = SiteContainer(site: site, updater: updater!) } - + let justSites = self.containers.mapValues { - return $0.site + $0.site } completion(justSites, nil) } } - - func deleteSite(id: String, callback: @escaping (Error?) -> ()) { - if let site = self.containers.removeValue(forKey: id) { + + func deleteSite(id: String, callback: @escaping (Error?) -> Void) { + if let site = containers.removeValue(forKey: id) { _ = KeyChain.delete(key: "\(site.site.id).dnCredentials") _ = KeyChain.delete(key: "\(site.site.id).key") - + do { let fileManager = FileManager.default let siteDir = try SiteList.getSiteDir(id: site.site.id) @@ -54,132 +54,132 @@ class Sites { } catch { print("Failed to delete site from fs: \(error.localizedDescription)") } - -#if !targetEnvironment(simulator) - site.site.manager!.removeFromPreferences(completionHandler: callback) - return -#endif + + #if !targetEnvironment(simulator) + site.site.manager!.removeFromPreferences(completionHandler: callback) + return + #endif } - + // Nothing to remove callback(nil) } - + func getSite(id: String) -> Site? { - return self.containers[id]?.site + return containers[id]?.site } - + func getUpdater(id: String) -> SiteUpdater? { - return self.containers[id]?.updater + return containers[id]?.updater } - + func getContainer(id: String) -> SiteContainer? { - return self.containers[id] + return containers[id] } } class SiteUpdater: NSObject, FlutterStreamHandler { - private var eventSink: FlutterEventSink?; - private var eventChannel: FlutterEventChannel; + private var eventSink: FlutterEventSink? + private var eventChannel: FlutterEventChannel private var site: Site private var notification: Any? public var startFunc: (() -> Void)? private var configFd: Int32? = nil private var configObserver: DispatchSourceFileSystemObject? = nil - + init(messenger: FlutterBinaryMessenger, site: Site) { do { let configPath = try SiteList.getSiteConfigFile(id: site.id, createDir: false) - self.configFd = open(configPath.path, O_EVTONLY) - self.configObserver = DispatchSource.makeFileSystemObjectSource( - fileDescriptor: self.configFd!, + configFd = open(configPath.path, O_EVTONLY) + configObserver = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: configFd!, eventMask: .write ) - + } catch { // SiteList.getSiteConfigFile should never throw because we are not creating it here - self.configObserver = nil + configObserver = nil } - + eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger) self.site = site super.init() - + eventChannel.setStreamHandler(self) - - self.configObserver?.setEventHandler(handler: self.configUpdated) - self.configObserver?.setCancelHandler { + + configObserver?.setEventHandler(handler: configUpdated) + configObserver?.setCancelHandler { if self.configFd != nil { close(self.configFd!) } self.configObserver = nil } - - self.configObserver?.resume() + + configObserver?.resume() } - + func setSite(site: Site) { self.site = site } /// onListen is called when flutter code attaches an event listener - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - eventSink = events; - -#if !targetEnvironment(simulator) - if site.manager == nil { - //TODO: The dn updater path seems to race to build a site that lacks a manager. The UI does not display this error - // and a another listen should occur and succeed. - return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil) - } - - self.notification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection , queue: nil) { n in - let oldConnected = self.site.connected - self.site.status = statusString[self.site.manager!.connection.status] - self.site.connected = statusMap[self.site.manager!.connection.status] - - // Check to see if we just moved to connected and if we have a start function to call when that happens - if self.site.connected! && oldConnected != self.site.connected && self.startFunc != nil { - self.startFunc!() - self.startFunc = nil + func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + eventSink = events + + #if !targetEnvironment(simulator) + if site.manager == nil { + // TODO: The dn updater path seems to race to build a site that lacks a manager. The UI does not display this error + // and a another listen should occur and succeed. + return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil) } - - self.update(connected: self.site.connected!) - } -#endif + + notification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection, queue: nil) { _ in + let oldConnected = self.site.connected + self.site.status = statusString[self.site.manager!.connection.status] + self.site.connected = statusMap[self.site.manager!.connection.status] + + // Check to see if we just moved to connected and if we have a start function to call when that happens + if self.site.connected!, oldConnected != self.site.connected, self.startFunc != nil { + self.startFunc!() + self.startFunc = nil + } + + self.update(connected: self.site.connected!) + } + #endif return nil } - + /// onCancel is called when the flutter listener stops listening - func onCancel(withArguments arguments: Any?) -> FlutterError? { - if (self.notification != nil) { - NotificationCenter.default.removeObserver(self.notification!) + func onCancel(withArguments _: Any?) -> FlutterError? { + if notification != nil { + NotificationCenter.default.removeObserver(notification!) } return nil } - + /// update is a way to send information to the flutter listener and generally should not be used directly func update(connected: Bool, replaceSite: Site? = nil) { - if (replaceSite != nil) { + if replaceSite != nil { site = replaceSite! } site.connected = connected site.status = connected ? "Connected" : "Disconnected" - + let encoder = JSONEncoder() let data = try! encoder.encode(site) - self.eventSink?(String(data: data, encoding: .utf8)) + eventSink?(String(data: data, encoding: .utf8)) } - + private func configUpdated() { - if self.site.connected != true { + if site.connected != true { return } - - guard let newSite = try? Site(manager: self.site.manager!) else { + + guard let newSite = try? Site(manager: site.manager!) else { return } - - self.update(connected: newSite.connected ?? false, replaceSite: newSite) + + update(connected: newSite.connected ?? false, replaceSite: newSite) } }