You've already forked ios-client
mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-05-22 17:10:12 -07:00
0327624b30
* fix layout on ipad * Create multi profiles * multi-profile server config, connection cache, and auth fixes Split ProfilesView into ProfilesListView, AddProfileSheet, ProfileBadge, AddProfileViewModel - Add server URL + setup key fields to Add Profile screen - Store per-profile connection data (ip/fqdn/managementURL) as typed model in ProfileConnectionCache - Show cached connection info immediately on profile switch; empty if no prior data - Fix profile deletion persistence via tombstone in profiles.json - Fix logout to remove both netbird.cfg and state.json (cfg holds auth tokens) - Preserve managementURL in UI after logout via cache fallback - Guard polling from overwriting new profile's data during disconnect/reconnect cycle * fix(multi-profile): reinitialize VPN adapter on profile switch and show current server URL * Update project.pbxproj * fix(multi-profile): prevent default server overwrite on logout/re-login * Update PacketTunnelProvider.swift * fix(multi-profile): address code review findings - Normalize only scheme and host to lowercase when saving management server URL; previously the full URL was lowercased which could corrupt case-sensitive paths on self-hosted servers - Move switchConnectionInfo(to:) inside the do-block so the connection UI is only updated after a successful profile switch - Add ProfileConnectionCache.remove(for:) and clearConnectionData(for:) to prevent stale ip/fqdn/managementURL from persisting after a profile is deleted or logged out; call them from removeProfile and logoutProfile - Use ASWebAuthenticationSession.Callback.customScheme on iOS 17.4+, falling back to the deprecated callbackURLScheme initializer on older versions; add a comment explaining why "http" is used and that a proper fix requires custom-scheme support in the SDK - Guard presentationAnchor against a missing key window with assertionFailure in debug builds instead of silently returning an empty UIWindow
239 lines
6.7 KiB
Swift
239 lines
6.7 KiB
Swift
//
|
|
// ConfigurationProvider.swift
|
|
// NetBird
|
|
//
|
|
// Protocol abstraction for platform-specific configuration management.
|
|
// iOS uses SDK file-based preferences, tvOS uses IPC-based config transfer.
|
|
//
|
|
|
|
import Foundation
|
|
import NetBirdSDK
|
|
|
|
// MARK: - Protocol Definition
|
|
|
|
/// Abstracts platform-specific configuration storage and retrieval.
|
|
/// - iOS: Uses NetBirdSDKPreferences with file-based storage in App Group container
|
|
/// - tvOS: Uses UserDefaults + IPC transfer (App Group files don't work between app/extension)
|
|
protocol ConfigurationProvider {
|
|
// MARK: - Rosenpass Settings
|
|
|
|
/// Whether Rosenpass (post-quantum encryption) is enabled
|
|
var rosenpassEnabled: Bool { get set }
|
|
|
|
/// Whether Rosenpass permissive mode is enabled (allows non-Rosenpass peers)
|
|
var rosenpassPermissive: Bool { get set }
|
|
|
|
// MARK: - Pre-Shared Key
|
|
|
|
/// The current pre-shared key (empty string if not set)
|
|
var preSharedKey: String { get set }
|
|
|
|
/// Whether a pre-shared key is configured
|
|
var hasPreSharedKey: Bool { get }
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
/// Commits any pending changes to persistent storage
|
|
/// Returns true on success, false on failure
|
|
@discardableResult
|
|
func commit() -> Bool
|
|
|
|
/// Reloads settings from persistent storage
|
|
func reload()
|
|
}
|
|
|
|
// MARK: - iOS Implementation
|
|
|
|
#if os(iOS)
|
|
/// iOS implementation using NetBirdSDKPreferences (file-based storage)
|
|
final class iOSConfigurationProvider: ConfigurationProvider {
|
|
|
|
private var preferences: NetBirdSDKPreferences
|
|
|
|
init() {
|
|
self.preferences = Preferences.newPreferences()
|
|
}
|
|
|
|
// MARK: - Rosenpass
|
|
|
|
var rosenpassEnabled: Bool {
|
|
get {
|
|
var result = ObjCBool(false)
|
|
do {
|
|
try preferences.getRosenpassEnabled(&result)
|
|
} catch {
|
|
print("ConfigurationProvider: Failed to read rosenpassEnabled - \(error)")
|
|
}
|
|
return result.boolValue
|
|
}
|
|
set {
|
|
preferences.setRosenpassEnabled(newValue)
|
|
}
|
|
}
|
|
|
|
var rosenpassPermissive: Bool {
|
|
get {
|
|
var result = ObjCBool(false)
|
|
do {
|
|
try preferences.getRosenpassPermissive(&result)
|
|
} catch {
|
|
print("ConfigurationProvider: Failed to read rosenpassPermissive - \(error)")
|
|
}
|
|
return result.boolValue
|
|
}
|
|
set {
|
|
preferences.setRosenpassPermissive(newValue)
|
|
}
|
|
}
|
|
|
|
// MARK: - Pre-Shared Key
|
|
|
|
var preSharedKey: String {
|
|
get {
|
|
return preferences.getPreSharedKey(nil)
|
|
}
|
|
set {
|
|
preferences.setPreSharedKey(newValue)
|
|
}
|
|
}
|
|
|
|
var hasPreSharedKey: Bool {
|
|
return !preSharedKey.isEmpty
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
@discardableResult
|
|
func commit() -> Bool {
|
|
do {
|
|
try preferences.commit()
|
|
return true
|
|
} catch {
|
|
print("ConfigurationProvider: Failed to commit - \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func reload() {
|
|
// Only recreate preferences if the config file exists.
|
|
// If the config was deleted by a logout, NetBirdSDKNewPreferences would create
|
|
// a new file with the default server URL (api.netbird.io), overwriting any
|
|
// saved custom server URL in netbird_server_url.
|
|
guard let configPath = Preferences.configFile(),
|
|
FileManager.default.fileExists(atPath: configPath) else {
|
|
return
|
|
}
|
|
self.preferences = Preferences.newPreferences()
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - tvOS Implementation
|
|
|
|
#if os(tvOS)
|
|
/// tvOS implementation that reads/writes settings directly to the config JSON.
|
|
/// This mirrors iOS behavior where all settings live in one config file.
|
|
/// The config JSON is stored in UserDefaults and sent to the extension via IPC.
|
|
final class tvOSConfigurationProvider: ConfigurationProvider {
|
|
|
|
init() {}
|
|
|
|
// MARK: - Rosenpass
|
|
|
|
var rosenpassEnabled: Bool {
|
|
get { extractJSONBool(field: "RosenpassEnabled") ?? false }
|
|
set { updateJSONField(field: "RosenpassEnabled", value: newValue) }
|
|
}
|
|
|
|
var rosenpassPermissive: Bool {
|
|
get { extractJSONBool(field: "RosenpassPermissive") ?? false }
|
|
set { updateJSONField(field: "RosenpassPermissive", value: newValue) }
|
|
}
|
|
|
|
// MARK: - Pre-Shared Key
|
|
|
|
var preSharedKey: String {
|
|
get { extractJSONString(field: "PreSharedKey") ?? "" }
|
|
set { updateJSONField(field: "PreSharedKey", value: newValue) }
|
|
}
|
|
|
|
var hasPreSharedKey: Bool {
|
|
return !preSharedKey.isEmpty
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
@discardableResult
|
|
func commit() -> Bool {
|
|
// Settings are written directly to config JSON, no separate commit needed
|
|
return true
|
|
}
|
|
|
|
func reload() {
|
|
// Config JSON is always read fresh from UserDefaults
|
|
}
|
|
|
|
// MARK: - JSON Helpers (read/write to stored config)
|
|
|
|
private func getConfigJSON() -> String? {
|
|
return Preferences.loadConfigFromUserDefaults()
|
|
}
|
|
|
|
private func saveConfigJSON(_ json: String) {
|
|
_ = Preferences.saveConfigToUserDefaults(json)
|
|
}
|
|
|
|
private func parseConfigDict() -> [String: Any]? {
|
|
guard let json = getConfigJSON(),
|
|
let data = json.data(using: .utf8),
|
|
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
return nil
|
|
}
|
|
return dict
|
|
}
|
|
|
|
private func extractJSONBool(field: String) -> Bool? {
|
|
return parseConfigDict()?[field] as? Bool
|
|
}
|
|
|
|
private func extractJSONString(field: String) -> String? {
|
|
return parseConfigDict()?[field] as? String
|
|
}
|
|
|
|
private func updateJSONField<T>(field: String, value: T) {
|
|
guard var dict = parseConfigDict() else {
|
|
AppLogger.shared.log("ConfigurationProvider: No config JSON available for updating '\(field)'")
|
|
return
|
|
}
|
|
|
|
guard dict[field] != nil else {
|
|
AppLogger.shared.log("ConfigurationProvider: Field '\(field)' not found in config JSON")
|
|
return
|
|
}
|
|
|
|
dict[field] = value
|
|
|
|
guard let data = try? JSONSerialization.data(withJSONObject: dict, options: [.sortedKeys]),
|
|
let json = String(data: data, encoding: .utf8) else {
|
|
AppLogger.shared.log("ConfigurationProvider: Failed to serialize config JSON")
|
|
return
|
|
}
|
|
|
|
saveConfigJSON(json)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - Factory
|
|
|
|
/// Factory for creating the appropriate ConfigurationProvider for the current platform
|
|
enum ConfigurationProviderFactory {
|
|
static func create() -> ConfigurationProvider {
|
|
#if os(iOS)
|
|
return iOSConfigurationProvider()
|
|
#else
|
|
return tvOSConfigurationProvider()
|
|
#endif
|
|
}
|
|
}
|