Format swift code

This commit is contained in:
Caleb Jasik 2025-02-20 15:20:17 -06:00
parent f1306d82d4
commit e87b7809b4
No known key found for this signature in database
9 changed files with 1569 additions and 1491 deletions

View file

@ -5,13 +5,13 @@ let groupName = "group.net.defined.mobileNebula"
class KeyChain { class KeyChain {
class func save(key: String, data: Data, managed: Bool) -> Bool { class func save(key: String, data: Data, managed: Bool) -> Bool {
var query: [String: Any] = [ var query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword as String, kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String : key, kSecAttrAccount as String: key,
kSecValueData as String : data, kSecValueData as String: data,
kSecAttrAccessGroup as String: groupName, kSecAttrAccessGroup as String: groupName,
] ]
if (managed) { if managed {
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
} }
@ -22,10 +22,10 @@ class KeyChain {
class func load(key: String) -> Data? { class func load(key: String) -> Data? {
let query: [String: Any] = [ let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword, kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String : key, kSecAttrAccount as String: key,
kSecReturnData as String : kCFBooleanTrue!, kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String : kSecMatchLimitOne, kSecMatchLimit as String: kSecMatchLimitOne,
kSecAttrAccessGroup as String: groupName, kSecAttrAccessGroup as String: groupName,
] ]
@ -42,8 +42,8 @@ class KeyChain {
class func delete(key: String) -> Bool { class func delete(key: String) -> Bool {
let query: [String: Any] = [ let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword as String, kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String : key, kSecAttrAccount as String: key,
kSecAttrAccessGroup as String: groupName, kSecAttrAccessGroup as String: groupName,
] ]
@ -56,7 +56,9 @@ extension Data {
init<T>(from value: T) { init<T>(from value: T) {
var value = value var value = value
var data = Data() var data = Data()
withUnsafePointer(to: &value, { (ptr: UnsafePointer<T>) -> Void in withUnsafePointer(
to: &value,
{ (ptr: UnsafePointer<T>) -> Void in
data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1)) data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1))
}) })
self.init(data) self.init(data)
@ -66,4 +68,3 @@ extension Data {
return self.withUnsafeBytes { $0.load(as: T.self) } return self.withUnsafeBytes { $0.load(as: T.self) }
} }
} }

View file

@ -1,7 +1,7 @@
import NetworkExtension
import MobileNebula import MobileNebula
import os.log import NetworkExtension
import SwiftyJSON import SwiftyJSON
import os.log
enum VPNStartError: Error { enum VPNStartError: Error {
case noManagers case noManagers
@ -23,7 +23,6 @@ extension AppMessageError: LocalizedError {
} }
} }
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
private var networkMonitor: NWPathMonitor? private var networkMonitor: NWPathMonitor?
@ -34,7 +33,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private var didSleep = false private var didSleep = false
private var cachedRouteDescription: String? 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 // 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 // `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 { if options?["expectStart"] != nil {
@ -55,7 +54,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
do { do {
// Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS // Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS
// TODO: Remove else once we drop support for iOS 16 // TODO: Remove else once we drop support for iOS 16
if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) { if ProcessInfo().isOperatingSystemAtLeast(
OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
{
manager = try await self.findManager() manager = try await self.findManager()
guard let foundManager = manager else { guard let foundManager = manager else {
throw VPNStartError.couldNotFindManager throw VPNStartError.couldNotFindManager
@ -87,16 +88,19 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// Make sure our ip is routed to the tun device // Make sure our ip is routed to the tun device
var err: NSError? var err: NSError?
let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err) let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR]) tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(
var routes: [NEIPv4Route] = [NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)] addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
var routes: [NEIPv4Route] = [
NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)
]
// Add our unsafe routes // Add our unsafe routes
try _site.unsafeRoutes.forEach { unsafeRoute in try _site.unsafeRoutes.forEach { unsafeRoute in
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err) let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)) routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR))
@ -107,7 +111,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
try await self.setTunnelNetworkSettings(tunnelNetworkSettings) try await self.setTunnelNetworkSettings(tunnelNetworkSettings)
var nebulaErr: NSError? var nebulaErr: NSError?
self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr) self.nebula = MobileNebulaNewNebula(
String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
self.startNetworkMonitor() self.startNetworkMonitor()
if nebulaErr != nil { if nebulaErr != nil {
@ -122,18 +127,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func handleDNUpdate(newSite: Site) { private func handleDNUpdate(newSite: Site) {
do { do {
self.site = newSite self.site = newSite
try self.nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey()) try self.nebula?.reload(
String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
} catch { } catch {
log.error("Got an error while updating nebula \(error.localizedDescription, privacy: .public)") 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) { // override func sleep(completionHandler: @escaping () -> Void) {
// nebula!.sleep() // nebula!.sleep()
// completionHandler() // completionHandler()
// } // }
private func findManager() async throws -> NETunnelProviderManager { private func findManager() async throws -> NETunnelProviderManager {
let targetProtoConfig = self.protocolConfiguration as? NETunnelProviderProtocol let targetProtoConfig = self.protocolConfiguration as? NETunnelProviderProtocol
@ -150,7 +157,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
throw VPNStartError.noProviderConfig throw VPNStartError.noProviderConfig
} }
let id = mgrProviderConfig["id"] as? String let id = mgrProviderConfig["id"] as? String
if (id == targetID) { if id == targetID {
return manager return manager
} }
} }
@ -170,7 +177,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
networkMonitor = nil networkMonitor = nil
} }
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { override func stopTunnel(
with reason: NEProviderStopReason, completionHandler: @escaping () -> Void
) {
nebula?.stop() nebula?.stop()
stopNetworkMonitor() stopNetworkMonitor()
completionHandler() completionHandler()
@ -181,7 +190,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
if routeDescription != cachedRouteDescription { if routeDescription != cachedRouteDescription {
// Don't bother to rebind if we don't have any gateways // Don't bother to rebind if we don't have any gateways
if routeDescription != "" { if routeDescription != "" {
nebula?.rebind("network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")") nebula?.rebind(
"network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")")
} }
cachedRouteDescription = routeDescription cachedRouteDescription = routeDescription
} }
@ -189,7 +199,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String { private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String {
var str: [String] = [] var str: [String] = []
endpoints.forEach{ endpoint in endpoints.forEach { endpoint in
switch endpoint { switch endpoint {
case let .hostPort(.ipv6(host), port): case let .hostPort(.ipv6(host), port):
str.append("[\(host)]:\(port)") str.append("[\(host)]:\(port)")
@ -222,7 +232,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
defer { defer {
self.cancelTunnelWithError(error) self.cancelTunnelWithError(error)
} }
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error.localizedDescription))) return try? JSONEncoder().encode(
IPCResponse.init(type: .error, message: JSON(error.localizedDescription)))
} }
} }
@ -244,8 +255,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
error = AppMessageError.unknownIPCType(command: call.command) error = AppMessageError.unknownIPCType(command: call.command)
} }
if (error != nil) { if error != nil {
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error"))) return try? JSONEncoder().encode(
IPCResponse.init(
type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
} else { } else {
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data)) return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data))
} }
@ -259,13 +272,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func getHostInfo(args: JSON) -> (JSON?, Error?) { private func getHostInfo(args: JSON) -> (JSON?, Error?) {
var err: NSError? var err: NSError?
let res = nebula!.getHostInfo(byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err) let res = nebula!.getHostInfo(
byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
return (JSON(res), err) return (JSON(res), err)
} }
private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) { private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) {
var err: NSError? var err: NSError?
let res = nebula!.setRemoteForTunnel(args["vpnIp"].string, addr: args["addr"].string, error: &err) let res = nebula!.setRemoteForTunnel(
args["vpnIp"].string, addr: args["addr"].string, error: &err)
return (JSON(res), err) return (JSON(res), err)
} }
@ -306,4 +321,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return nil return nil
} }
} }

View file

@ -1,12 +1,12 @@
import NetworkExtension
import MobileNebula import MobileNebula
import NetworkExtension
import SwiftyJSON import SwiftyJSON
import os.log import os.log
let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site") let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site")
enum SiteError: Error { enum SiteError: Error {
case nonConforming(site: [String : Any]?) case nonConforming(site: [String: Any]?)
case noCertificate case noCertificate
case keyLoad case keyLoad
case keySave case keySave
@ -131,7 +131,7 @@ struct CertificateValidity: Codable {
} }
} }
let statusMap: Dictionary<NEVPNStatus, Bool> = [ let statusMap: [NEVPNStatus: Bool] = [
NEVPNStatus.invalid: false, NEVPNStatus.invalid: false,
NEVPNStatus.disconnected: false, NEVPNStatus.disconnected: false,
NEVPNStatus.connecting: false, NEVPNStatus.connecting: false,
@ -140,7 +140,7 @@ let statusMap: Dictionary<NEVPNStatus, Bool> = [
NEVPNStatus.disconnecting: true, NEVPNStatus.disconnecting: true,
] ]
let statusString: Dictionary<NEVPNStatus, String> = [ let statusString: [NEVPNStatus: String] = [
NEVPNStatus.invalid: "Invalid configuration", NEVPNStatus.invalid: "Invalid configuration",
NEVPNStatus.disconnected: "Disconnected", NEVPNStatus.disconnected: "Disconnected",
NEVPNStatus.connecting: "Connecting...", NEVPNStatus.connecting: "Connecting...",
@ -156,7 +156,7 @@ class Site: Codable {
var id: String var id: String
// Stored in proto // Stored in proto
var staticHostmap: Dictionary<String, StaticHosts> var staticHostmap: [String: StaticHosts]
var unsafeRoutes: [UnsafeRoute] var unsafeRoutes: [UnsafeRoute]
var cert: CertificateInfo? var cert: CertificateInfo?
var ca: [CertificateInfo] var ca: [CertificateInfo]
@ -244,18 +244,18 @@ class Site: Codable {
do { do {
let rawCert = incoming.cert let rawCert = incoming.cert
let rawDetails = MobileNebulaParseCerts(rawCert, &err) let rawDetails = MobileNebulaParseCerts(rawCert, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
var certs: [CertificateInfo] var certs: [CertificateInfo]
certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!) certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!)
if (certs.count == 0) { if certs.count == 0 {
throw SiteError.noCertificate throw SiteError.noCertificate
} }
cert = certs[0] cert = certs[0]
if (!cert!.validity.valid) { if !cert!.validity.valid {
errors.append("Certificate is invalid: \(cert!.validity.reason)") errors.append("Certificate is invalid: \(cert!.validity.reason)")
} }
@ -266,19 +266,19 @@ class Site: Codable {
do { do {
let rawCa = incoming.ca let rawCa = incoming.ca
let rawCaDetails = MobileNebulaParseCerts(rawCa, &err) let rawCaDetails = MobileNebulaParseCerts(rawCa, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!) ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!)
var hasErrors = false var hasErrors = false
ca.forEach { cert in ca.forEach { cert in
if (!cert.validity.valid) { if !cert.validity.valid {
hasErrors = true hasErrors = true
} }
} }
if (hasErrors && !managed) { if hasErrors && !managed {
errors.append("There are issues with 1 or more ca certificates") errors.append("There are issues with 1 or more ca certificates")
} }
@ -294,11 +294,11 @@ class Site: Codable {
errors.append("Unable to create the site directory: \(error.localizedDescription)") 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") errors.append("Unable to fetch managed updates - please re-enroll the device")
} }
if (errors.isEmpty) { if errors.isEmpty {
do { do {
let encoder = JSONEncoder() let encoder = JSONEncoder()
let rawConfig = try encoder.encode(incoming) let rawConfig = try encoder.encode(incoming)
@ -307,7 +307,7 @@ class Site: Codable {
var err: NSError? var err: NSError?
MobileNebulaTestConfig(strConfig, key, &err) MobileNebulaTestConfig(strConfig, key, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
} catch { } catch {
@ -327,7 +327,7 @@ class Site: Codable {
} }
func getDNCredentials() throws -> DNCredentials { func getDNCredentials() throws -> DNCredentials {
if (!managed) { if !managed {
throw SiteError.unmanagedGetCredentials throw SiteError.unmanagedGetCredentials
} }
@ -344,7 +344,7 @@ class Site: Codable {
let creds = try getDNCredentials() let creds = try getDNCredentials()
creds.invalid = true creds.invalid = true
if (!(try creds.save(siteID: self.id))) { if !(try creds.save(siteID: self.id)) {
throw SiteError.dnCredentialLoad throw SiteError.dnCredentialLoad
} }
} }
@ -353,7 +353,7 @@ class Site: Codable {
let creds = try getDNCredentials() let creds = try getDNCredentials()
creds.invalid = false creds.invalid = false
if (!(try creds.save(siteID: self.id))) { if !(try creds.save(siteID: self.id)) {
throw SiteError.dnCredentialSave throw SiteError.dnCredentialSave
} }
} }
@ -429,7 +429,7 @@ class DNCredentials: Codable {
struct IncomingSite: Codable { struct IncomingSite: Codable {
var name: String var name: String
var id: String var id: String
var staticHostmap: Dictionary<String, StaticHosts> var staticHostmap: [String: StaticHosts]
var unsafeRoutes: [UnsafeRoute]? var unsafeRoutes: [UnsafeRoute]?
var cert: String? var cert: String?
var ca: String? var ca: String?
@ -456,7 +456,10 @@ struct IncomingSite: Codable {
return try encoder.encode(config) 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 let configPath: URL
do { do {
@ -469,15 +472,15 @@ struct IncomingSite: Codable {
log.notice("Saving to \(configPath, privacy: .public)") log.notice("Saving to \(configPath, privacy: .public)")
do { do {
if (self.key != nil) { if self.key != nil {
let data = self.key!.data(using: .utf8) let data = self.key!.data(using: .utf8)
if (!KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false)) { if !KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false) {
return callback(SiteError.keySave) return callback(SiteError.keySave)
} }
} }
do { do {
if ((try self.dnCredentials?.save(siteID: self.id)) == false) { if (try self.dnCredentials?.save(siteID: self.id)) == false {
return callback(SiteError.dnCredentialSave) return callback(SiteError.dnCredentialSave)
} }
} catch { } catch {
@ -490,24 +493,25 @@ struct IncomingSite: Codable {
return callback(error) return callback(error)
} }
#if targetEnvironment(simulator)
#if targetEnvironment(simulator)
// We are on a simulator and there is no NEVPNManager for us to interact with // We are on a simulator and there is no NEVPNManager for us to interact with
callback(nil) callback(nil)
#else #else
if saveToManager { if saveToManager {
self.saveToManager(manager: manager, callback: callback) self.saveToManager(manager: manager, callback: callback)
} else { } else {
callback(nil) callback(nil)
} }
#endif #endif
} }
private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) { private func saveToManager(
if (manager != nil) { manager: NETunnelProviderManager?, callback: @escaping (Error?) -> Void
) {
if manager != nil {
// We need to refresh our settings to properly update config // We need to refresh our settings to properly update config
manager?.loadFromPreferences { error in manager?.loadFromPreferences { error in
if (error != nil) { if error != nil {
return callback(error) return callback(error)
} }
@ -519,10 +523,13 @@ struct IncomingSite: Codable {
return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback) 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 // Stuff our details in the protocol
let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() let proto =
proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension"; manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension"
// WARN: If we stop setting providerConfiguration["id"] here, we'll need to use something else to match // WARN: If we stop setting providerConfiguration["id"] here, we'll need to use something else to match
// managers in PacketTunnelProvider.findManager // managers in PacketTunnelProvider.findManager
proto.providerConfiguration = ["id": self.id] proto.providerConfiguration = ["id": self.id]
@ -536,7 +543,7 @@ struct IncomingSite: Codable {
manager.localizedDescription = self.name manager.localizedDescription = self.name
manager.isEnabled = true manager.isEnabled = true
manager.saveToPreferences{ error in manager.saveToPreferences { error in
return callback(error) return callback(error)
} }
} }

View file

@ -6,9 +6,10 @@ class SiteList {
/// Gets the root directory that can be used to share files between the UI and VPN process. Does ensure the directory exists /// 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 { static func getRootDir() throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")! 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) try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true)
} }
@ -19,7 +20,7 @@ class SiteList {
static func getSitesDir() throws -> URL { static func getSitesDir() throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true) 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) try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true)
} }
return sitesDir return sitesDir
@ -29,7 +30,7 @@ class SiteList {
static func getSiteDir(id: String, create: Bool = false) throws -> URL { static func getSiteDir(id: String, create: Bool = false) throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true) 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) try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true)
} }
return siteDir return siteDir
@ -37,39 +38,43 @@ class SiteList {
/// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json /// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json
static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL { static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL {
return try getSiteDir(id: id, create: createDir).appendingPathComponent("config", isDirectory: false).appendingPathExtension("json") 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 /// Gets the file that represents the site log output, $rootDir/sites/$siteID/log
static func getSiteLogFile(id: String, createDir: Bool) throws -> URL { static func getSiteLogFile(id: String, createDir: Bool) throws -> URL {
return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false) return try getSiteDir(id: id, create: createDir).appendingPathComponent(
"logs", isDirectory: false)
} }
init(completion: @escaping ([String: Site]?, Error?) -> ()) { init(completion: @escaping ([String: Site]?, Error?) -> Void) {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
SiteList.loadAllFromFS { sites, err in SiteList.loadAllFromFS { sites, err in
if sites != nil { if sites != nil {
self.sites = sites! self.sites = sites!
} }
completion(sites, err) completion(sites, err)
} }
#else #else
SiteList.loadAllFromNETPM { sites, err in SiteList.loadAllFromNETPM { sites, err in
if sites != nil { if sites != nil {
self.sites = sites! 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 let fileManager = FileManager.default
var siteDirs: [URL] var siteDirs: [URL]
var sites = [String: Site]() var sites = [String: Site]()
do { do {
siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil) siteDirs = try fileManager.contentsOfDirectory(
at: getSitesDir(), includingPropertiesForKeys: nil)
} catch { } catch {
completion(nil, error) completion(nil, error)
@ -78,7 +83,8 @@ class SiteList {
siteDirs.forEach { path in siteDirs.forEach { path in
do { do {
let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json")) let site = try Site(
path: path.appendingPathComponent("config").appendingPathExtension("json"))
sites[site.id] = site sites[site.id] = site
} catch { } catch {
@ -91,15 +97,15 @@ class SiteList {
completion(sites, nil) 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]() var sites = [String: Site]()
// dispatchGroup is used to ensure we have migrated all sites before returning them // 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 // If there are no sites to migrate, there are never any entrants
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
NETunnelProviderManager.loadAllFromPreferences() { newManagers, err in NETunnelProviderManager.loadAllFromPreferences { newManagers, err in
if (err != nil) { if err != nil {
return completion(nil, err) return completion(nil, err)
} }

View file

@ -10,7 +10,8 @@ class APIClient {
init() { init() {
let packageInfo = PackageInfo() let packageInfo = PackageInfo()
apiClient = MobileNebulaNewAPIClient("MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")! apiClient = MobileNebulaNewAPIClient(
"MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
} }
func enroll(code: String) throws -> IncomingSite { func enroll(code: String) throws -> IncomingSite {
@ -18,7 +19,9 @@ class APIClient {
return try decodeIncomingSite(jsonSite: res.site) return try decodeIncomingSite(jsonSite: res.site)
} }
func tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String) throws -> IncomingSite? { func tryUpdate(
siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String
) throws -> IncomingSite? {
let res: MobileNebulaTryUpdateResult let res: MobileNebulaTryUpdateResult
do { do {
res = try apiClient.tryUpdate( res = try apiClient.tryUpdate(
@ -29,14 +32,14 @@ class APIClient {
trustedKeys: trustedKeys) trustedKeys: trustedKeys)
} catch { } catch {
// type information from Go is not available, use string matching instead // 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 APIClientError.invalidCredentials
} }
throw error throw error
} }
if (res.fetchedUpdate) { if res.fetchedUpdate {
return try decodeIncomingSite(jsonSite: res.site) return try decodeIncomingSite(jsonSite: res.site)
} }

View file

@ -1,8 +1,8 @@
import UIKit
import Flutter import Flutter
import MobileNebula import MobileNebula
import NetworkExtension import NetworkExtension
import SwiftyJSON import SwiftyJSON
import UIKit
enum ChannelName { enum ChannelName {
static let vpn = "net.defined.mobileNebula/NebulaVpnService" static let vpn = "net.defined.mobileNebula/NebulaVpnService"
@ -19,18 +19,16 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
private var sites: Sites? private var sites: Sites?
private var ui: FlutterMethodChannel? private var ui: FlutterMethodChannel?
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
dnUpdater.updateAllLoop { site in dnUpdater.updateAllLoop { site in
// Signal the site has changed in case the current site details screen is active // Signal the site has changed in case the current site details screen is active
let container = self.sites?.getContainer(id: site.id) let container = self.sites?.getContainer(id: site.id)
if (container != nil) { if container != nil {
// Update references to the site with the new site config // Update references to the site with the new site config
container!.site = site container!.site = site
container!.updater.update(connected: site.connected ?? false, replaceSite: site) container!.updater.update(connected: site.connected ?? false, replaceSite: site)
@ -47,7 +45,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
sites = Sites(messenger: controller.binaryMessenger) sites = Sites(messenger: controller.binaryMessenger)
ui = FlutterMethodChannel(name: ChannelName.vpn, binaryMessenger: 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) -> Void in
switch call.method { switch call.method {
case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result) case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result)
case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result) case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result)
@ -62,11 +60,16 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
case "startSite": return self.startSite(call: call, result: result) case "startSite": return self.startSite(call: call, result: result)
case "stopSite": return self.stopSite(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.listHostmap":
case "active.listPendingHostmap": self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result) self.vpnRequest(command: "listHostmap", arguments: call.arguments, result: result)
case "active.getHostInfo": self.vpnRequest(command: "getHostInfo", arguments: call.arguments, result: result) case "active.listPendingHostmap":
case "active.setRemoteForTunnel": self.vpnRequest(command: "setRemoteForTunnel", arguments: call.arguments, result: result) self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result)
case "active.closeTunnel": self.vpnRequest(command: "closeTunnel", 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: default:
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
@ -77,28 +80,39 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
} }
func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) { func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> 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")) } guard let certs = args["certs"] else {
return result(MissingArgumentError(message: "certs is a required argument"))
}
var err: NSError? var err: NSError?
let json = MobileNebulaParseCerts(certs, &err) 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(
CallFailedError(
message: "Error while parsing certificate(s)", details: err!.localizedDescription))
} }
return result(json) return result(json)
} }
func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) { func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> 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 cert = args["cert"] else {
guard let key = args["key"] else { return result(MissingArgumentError(message: "key is a required argument")) } 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 err: NSError?
var validd: ObjCBool = false var validd: ObjCBool = false
let valid = MobileNebulaVerifyCertAndKey(cert, key, &validd, &err) 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(
CallFailedError(
message: "Error while verifying certificate and private key",
details: err!.localizedDescription))
} }
return result(valid) return result(valid)
@ -107,8 +121,10 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
func nebulaGenerateKeyPair(result: FlutterResult) { func nebulaGenerateKeyPair(result: FlutterResult) {
var err: NSError? var err: NSError?
let kp = MobileNebulaGenerateKeyPair(&err) let kp = MobileNebulaGenerateKeyPair(&err)
if (err != nil) { if err != nil {
return result(CallFailedError(message: "Error while generating key pairs", details: err!.localizedDescription)) return result(
CallFailedError(
message: "Error while generating key pairs", details: err!.localizedDescription))
} }
return result(kp) return result(kp)
@ -119,8 +135,10 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
var err: NSError? var err: NSError?
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err) let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
if (err != nil) { if err != nil {
return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription)) return result(
CallFailedError(message: "Error while rendering config", details: err!.localizedDescription)
)
} }
return result(yaml) return result(yaml)
@ -134,21 +152,24 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
let oldSite = self.sites?.getSite(id: site.id) let oldSite = self.sites?.getSite(id: site.id)
site.save(manager: oldSite?.manager) { error in site.save(manager: oldSite?.manager) { error in
if (error != nil) { if error != nil {
return result(CallFailedError(message: "Failed to enroll", details: error!.localizedDescription)) return result(
CallFailedError(message: "Failed to enroll", details: error!.localizedDescription))
} }
result(nil) result(nil)
} }
} catch { } catch {
return result(CallFailedError(message: "Error from DN api", details: error.localizedDescription)) return result(
CallFailedError(message: "Error from DN api", details: error.localizedDescription))
} }
} }
func listSites(result: @escaping FlutterResult) { func listSites(result: @escaping FlutterResult) {
self.sites?.loadSites { (sites, err) -> () in self.sites?.loadSites { (sites, err) -> Void in
if (err != nil) { if err != nil {
return result(CallFailedError(message: "Failed to load site list", details: err!.localizedDescription)) return result(
CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
} }
let encoder = JSONEncoder() let encoder = JSONEncoder()
@ -162,8 +183,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
guard let id = call.arguments as? String else { return result(NoArgumentsError()) } guard let id = call.arguments as? String else { return result(NoArgumentsError()) }
//TODO: stop the site if its running currently //TODO: stop the site if its running currently
self.sites?.deleteSite(id: id) { error in self.sites?.deleteSite(id: id) { error in
if (error != nil) { if error != nil {
result(CallFailedError(message: "Failed to delete site", details: error!.localizedDescription)) result(
CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
} }
result(nil) result(nil)
@ -180,8 +202,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
let oldSite = self.sites?.getSite(id: site.id) let oldSite = self.sites?.getSite(id: site.id)
site.save(manager: oldSite?.manager) { error in site.save(manager: oldSite?.manager) { error in
if (error != nil) { if error != nil {
return result(CallFailedError(message: "Failed to save site", details: error!.localizedDescription)) return result(
CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
} }
self.sites?.loadSites { _, _ in self.sites?.loadSites { _, _ in
@ -191,59 +214,67 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
} }
func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) { func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> 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")) } guard let id = args["id"] else {
return result(MissingArgumentError(message: "id is a required argument"))
}
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
let updater = self.sites?.getUpdater(id: id) let updater = self.sites?.getUpdater(id: id)
updater?.update(connected: true) updater?.update(connected: true)
#else #else
let container = self.sites?.getContainer(id: id) let container = self.sites?.getContainer(id: id)
let manager = container?.site.manager let manager = container?.site.manager
manager?.loadFromPreferences{ error in manager?.loadFromPreferences { error in
//TODO: Handle load error //TODO: Handle load error
// This is silly but we need to enable the site each time to avoid situations where folks have multiple sites // This is silly but we need to enable the site each time to avoid situations where folks have multiple sites
manager?.isEnabled = true manager?.isEnabled = true
manager?.saveToPreferences{ error in manager?.saveToPreferences { error in
//TODO: Handle load error //TODO: Handle load error
manager?.loadFromPreferences{ error in manager?.loadFromPreferences { error in
//TODO: Handle load error //TODO: Handle load error
do { do {
container?.updater.startFunc = {() -> Void in container?.updater.startFunc = { () -> Void in
return self.vpnRequest(command: "start", arguments: args, result: result) return self.vpnRequest(command: "start", arguments: args, result: result)
} }
try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)]) try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)])
} catch { } catch {
return result(CallFailedError(message: "Could not start site", details: error.localizedDescription)) return result(
CallFailedError(
message: "Could not start site", details: error.localizedDescription))
} }
} }
} }
} }
#endif #endif
} }
func stopSite(call: FlutterMethodCall, result: @escaping FlutterResult) { func stopSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> 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")) } guard let id = args["id"] else {
#if targetEnvironment(simulator) return result(MissingArgumentError(message: "id is a required argument"))
}
#if targetEnvironment(simulator)
let updater = self.sites?.getUpdater(id: id) let updater = self.sites?.getUpdater(id: id)
updater?.update(connected: false) updater?.update(connected: false)
#else #else
let manager = self.sites?.getSite(id: id)?.manager let manager = self.sites?.getSite(id: id)?.manager
manager?.loadFromPreferences{ error in manager?.loadFromPreferences { error in
//TODO: Handle load error //TODO: Handle load error
manager?.connection.stopVPNTunnel() manager?.connection.stopVPNTunnel()
return result(nil) return result(nil)
} }
#endif #endif
} }
func vpnRequest(command: String, arguments: Any?, result: @escaping FlutterResult) { func vpnRequest(command: String, arguments: Any?, result: @escaping FlutterResult) {
guard let args = arguments as? Dictionary<String, Any> 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")) } guard let id = args["id"] as? String else {
return result(MissingArgumentError(message: "id is a required argument"))
}
let container = sites?.getContainer(id: id) let container = sites?.getContainer(id: id)
if container == nil { if container == nil {
@ -258,7 +289,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
if let session = container!.site.manager?.connection as? NETunnelProviderSession { if let session = container!.site.manager?.connection as? NETunnelProviderSession {
do { do {
try session.sendProviderMessage(try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))) { data in try session.sendProviderMessage(
try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))
) { data in
if data == nil { if data == nil {
return result(nil) return result(nil)
} }
@ -272,7 +305,8 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
return result(res.message?.object) return result(res.message?.object)
} }
return result(CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error")) return result(
CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
} }
} catch { } catch {
return result(CallFailedError(message: error.localizedDescription)) return result(CallFailedError(message: error.localizedDescription))
@ -288,7 +322,10 @@ func MissingArgumentError(message: String, details: Error? = nil) -> FlutterErro
return FlutterError(code: "missingArgument", message: message, details: details) return FlutterError(code: "missingArgument", message: message, details: details)
} }
func NoArgumentsError(message: String? = "no arguments were provided or could not be deserialized", details: Error? = nil) -> FlutterError { func NoArgumentsError(
message: String? = "no arguments were provided or could not be deserialized",
details: Error? = nil
) -> FlutterError {
return FlutterError(code: "noArguments", message: message, details: details) return FlutterError(code: "noArguments", message: message, details: details)
} }

View file

@ -6,13 +6,13 @@ class DNUpdater {
private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater") private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater")
func updateAll(onUpdate: @escaping (Site) -> ()) { func updateAll(onUpdate: @escaping (Site) -> Void) {
_ = SiteList{ (sites, _) -> () in _ = SiteList { (sites, _) -> Void in
// NEVPN seems to force us onto the main thread and we are about to make network calls that // 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. // could block for a while. Push ourselves onto another thread to avoid blocking the UI.
Task.detached(priority: .userInitiated) { Task.detached(priority: .userInitiated) {
sites?.values.forEach { site in 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 // The vpn service is in charge of updating the currently connected site
return return
} }
@ -23,23 +23,23 @@ class DNUpdater {
} }
} }
func updateAllLoop(onUpdate: @escaping (Site) -> ()) { func updateAllLoop(onUpdate: @escaping (Site) -> Void) {
timer.eventHandler = { timer.eventHandler = {
self.updateAll(onUpdate: onUpdate) self.updateAll(onUpdate: onUpdate)
} }
timer.resume() timer.resume()
} }
func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> ()) { func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> Void) {
timer.eventHandler = { timer.eventHandler = {
self.updateSite(site: site, onUpdate: onUpdate) self.updateSite(site: site, onUpdate: onUpdate)
} }
timer.resume() timer.resume()
} }
func updateSite(site: Site, onUpdate: @escaping (Site) -> ()) { func updateSite(site: Site, onUpdate: @escaping (Site) -> Void) {
do { do {
if (!site.managed) { if !site.managed {
return return
} }
@ -55,7 +55,7 @@ class DNUpdater {
trustedKeys: credentials.trustedKeys trustedKeys: credentials.trustedKeys
) )
} catch (APIClientError.invalidCredentials) { } catch (APIClientError.invalidCredentials) {
if (!credentials.invalid) { if !credentials.invalid {
try site.invalidateDNCredentials() try site.invalidateDNCredentials()
log.notice("Invalidated credentials in site: \(site.name, privacy: .public)") log.notice("Invalidated credentials in site: \(site.name, privacy: .public)")
} }
@ -64,10 +64,13 @@ class DNUpdater {
} }
let siteManager = site.manager let siteManager = site.manager
let shouldSaveToManager = siteManager != nil || ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) let shouldSaveToManager =
siteManager != nil
|| ProcessInfo().isOperatingSystemAtLeast(
OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
newSite?.save(manager: site.manager, saveToManager: shouldSaveToManager) { error in 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)") self.log.error("failed to save update: \(error!.localizedDescription, privacy: .public)")
} }
@ -75,13 +78,15 @@ class DNUpdater {
onUpdate(Site(incoming: newSite!)) onUpdate(Site(incoming: newSite!))
} }
if (credentials.invalid) { if credentials.invalid {
try site.validateDNCredentials() try site.validateDNCredentials()
log.notice("Revalidated credentials in site \(site.name, privacy: .public)") log.notice("Revalidated credentials in site \(site.name, privacy: .public)")
} }
} catch { } catch {
log.error("Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)") log.error(
"Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)"
)
} }
} }
} }

View file

@ -2,11 +2,10 @@ import Foundation
class PackageInfo { class PackageInfo {
func getVersion() -> String { func getVersion() -> String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
"unknown"
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
if (buildNumber == nil) { if buildNumber == nil {
return version return version
} }
@ -14,9 +13,8 @@ class PackageInfo {
} }
func getName() -> String { func getName() -> String {
return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? Bundle.main
Bundle.main.infoDictionary?["CFBundleName"] as? String ?? .infoDictionary?["CFBundleName"] as? String ?? "Nebula"
"Nebula"
} }
func getSystemVersion() -> String { func getSystemVersion() -> String {

View file

@ -1,5 +1,5 @@
import NetworkExtension
import MobileNebula import MobileNebula
import NetworkExtension
class SiteContainer { class SiteContainer {
var site: Site var site: Site
@ -19,15 +19,15 @@ class Sites {
self.messenger = messenger self.messenger = messenger
} }
func loadSites(completion: @escaping ([String: Site]?, Error?) -> ()) { func loadSites(completion: @escaping ([String: Site]?, Error?) -> Void) {
_ = SiteList { (sites, err) in _ = SiteList { (sites, err) in
if (err != nil) { if err != nil {
return completion(nil, err) return completion(nil, err)
} }
sites?.values.forEach{ site in sites?.values.forEach { site in
var updater = self.containers[site.id]?.updater var updater = self.containers[site.id]?.updater
if (updater != nil) { if updater != nil {
updater!.setSite(site: site) updater!.setSite(site: site)
} else { } else {
updater = SiteUpdater(messenger: self.messenger!, site: site) updater = SiteUpdater(messenger: self.messenger!, site: site)
@ -42,7 +42,7 @@ class Sites {
} }
} }
func deleteSite(id: String, callback: @escaping (Error?) -> ()) { func deleteSite(id: String, callback: @escaping (Error?) -> Void) {
if let site = self.containers.removeValue(forKey: id) { if let site = self.containers.removeValue(forKey: id) {
_ = KeyChain.delete(key: "\(site.site.id).dnCredentials") _ = KeyChain.delete(key: "\(site.site.id).dnCredentials")
_ = KeyChain.delete(key: "\(site.site.id).key") _ = KeyChain.delete(key: "\(site.site.id).key")
@ -55,10 +55,10 @@ class Sites {
print("Failed to delete site from fs: \(error.localizedDescription)") print("Failed to delete site from fs: \(error.localizedDescription)")
} }
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
site.site.manager!.removeFromPreferences(completionHandler: callback) site.site.manager!.removeFromPreferences(completionHandler: callback)
return return
#endif #endif
} }
// Nothing to remove // Nothing to remove
@ -79,8 +79,8 @@ class Sites {
} }
class SiteUpdater: NSObject, FlutterStreamHandler { class SiteUpdater: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?; private var eventSink: FlutterEventSink?
private var eventChannel: FlutterEventChannel; private var eventChannel: FlutterEventChannel
private var site: Site private var site: Site
private var notification: Any? private var notification: Any?
public var startFunc: (() -> Void)? public var startFunc: (() -> Void)?
@ -101,7 +101,8 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
self.configObserver = nil self.configObserver = nil
} }
eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger) eventChannel = FlutterEventChannel(
name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
self.site = site self.site = site
super.init() super.init()
@ -123,17 +124,23 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
} }
/// onListen is called when flutter code attaches an event listener /// onListen is called when flutter code attaches an event listener
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
eventSink = events; -> FlutterError?
{
eventSink = events
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
if site.manager == nil { 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 //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. // and a another listen should occur and succeed.
return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil) 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 self.notification = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection,
queue: nil
) { n in
let oldConnected = self.site.connected let oldConnected = self.site.connected
self.site.status = statusString[self.site.manager!.connection.status] self.site.status = statusString[self.site.manager!.connection.status]
self.site.connected = statusMap[self.site.manager!.connection.status] self.site.connected = statusMap[self.site.manager!.connection.status]
@ -146,13 +153,13 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
self.update(connected: self.site.connected!) self.update(connected: self.site.connected!)
} }
#endif #endif
return nil return nil
} }
/// onCancel is called when the flutter listener stops listening /// onCancel is called when the flutter listener stops listening
func onCancel(withArguments arguments: Any?) -> FlutterError? { func onCancel(withArguments arguments: Any?) -> FlutterError? {
if (self.notification != nil) { if self.notification != nil {
NotificationCenter.default.removeObserver(self.notification!) NotificationCenter.default.removeObserver(self.notification!)
} }
return nil return nil
@ -160,7 +167,7 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
/// update is a way to send information to the flutter listener and generally should not be used directly /// 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) { func update(connected: Bool, replaceSite: Site? = nil) {
if (replaceSite != nil) { if replaceSite != nil {
site = replaceSite! site = replaceSite!
} }
site.connected = connected site.connected = connected