diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index a4e4c8fc..3dfd9423 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */; }; 0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E0C072B236087A100155AAC /* InfoPlist.strings */; }; 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12BC8E27F62C8500B2F912 /* Validators.swift */; }; + 0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293850285A70AC002A6E0E /* AppPreference.swift */; }; 0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */; }; 0E2A8D4F27B04BBA00207D04 /* OrganizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */; }; 0E2AC24522EC3AC10037B4B0 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */; }; @@ -47,7 +48,6 @@ 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */; }; 0E5349C627C176C200C71BB3 /* EndpointView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */; }; 0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */; }; - 0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E53E63627E34FE2001D4902 /* AppContext.swift */; }; 0E5683B927C2825D00EAF1CD /* DiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5683B827C2825D00EAF1CD /* DiagnosticsView.swift */; }; 0E6059CB27FCC5DE003F4063 /* Flags.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E6059C827FCC5DD003F4063 /* Flags.xcassets */; }; 0E6059CC27FCC5DE003F4063 /* Providers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E6059C927FCC5DE003F4063 /* Providers.xcassets */; }; @@ -123,6 +123,7 @@ 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; }; 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; }; 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */; }; + 0EF2CC03285AFED800E501D5 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2CC02285AFED800E501D5 /* AppContext.swift */; }; 0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */; }; /* End PBXBuildFile section */ @@ -202,6 +203,7 @@ 0E12BC8E27F62C8500B2F912 /* Validators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validators.swift; sourceTree = ""; }; 0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; 0E23B4A12298559800304C30 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + 0E293850285A70AC002A6E0E /* AppPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreference.swift; sourceTree = ""; }; 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassepartoutApp.swift; sourceTree = ""; }; 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizerView.swift; sourceTree = ""; }; 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -232,7 +234,6 @@ 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledPicker.swift; sourceTree = ""; }; 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+OpenVPN.swift"; sourceTree = ""; }; 0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+WireGuard.swift"; sourceTree = ""; }; - 0E53E63627E34FE2001D4902 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; 0E5683B827C2825D00EAF1CD /* DiagnosticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsView.swift; sourceTree = ""; }; 0E57F63820C83FC5008323CF /* Passepartout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Passepartout.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0E57F64720C83FC7008323CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -341,6 +342,7 @@ 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = ""; }; 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = ""; }; 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = ""; }; + 0EF2CC02285AFED800E501D5 /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -565,7 +567,8 @@ 0EB17EA027D2263700D473B5 /* Constants */ = { isa = PBXGroup; children = ( - 0E53E63627E34FE2001D4902 /* AppContext.swift */, + 0EF2CC02285AFED800E501D5 /* AppContext.swift */, + 0E293850285A70AC002A6E0E /* AppPreference.swift */, 0EB17EA527D2263700D473B5 /* Constants+Extensions.swift */, 0E6059CE27FCC618003F4063 /* SwiftGen+Assets.swift */, 0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */, @@ -907,7 +910,6 @@ 0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */, 0E3CD47F280DA14B007075C0 /* AddProfileMenu.swift in Sources */, 0EB17EAA27D226C900D473B5 /* Constants+Extensions.swift in Sources */, - 0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */, 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */, 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */, 0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */, @@ -930,8 +932,10 @@ 0E71ACEF27C106B500F85C4B /* ProviderPresetView.swift in Sources */, 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */, 0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */, + 0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */, 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */, 0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */, + 0EF2CC03285AFED800E501D5 /* AppContext.swift in Sources */, 0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */, 0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */, 0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */, diff --git a/Passepartout/App/Constants/AppContext.swift b/Passepartout/App/Constants/AppContext.swift index 473af572..b7692a48 100644 --- a/Passepartout/App/Constants/AppContext.swift +++ b/Passepartout/App/Constants/AppContext.swift @@ -33,6 +33,8 @@ import PassepartoutServices class AppContext { static let shared = AppContext() + private let logManager: LogManager + private let profilesPersistence: Persistence private let providersPersistence: Persistence @@ -45,7 +47,7 @@ class AppContext { providersPersistence.containerURLs } - let appManager: AppManager + let upgradeManager: UpgradeManager let providerManager: ProviderManager @@ -62,18 +64,15 @@ class AppContext { private var cancellables: Set = [] private init() { - - // core - - appManager = AppManager() - appManager.logLevel = Constants.Log.logLevel - appManager.logFile = Constants.Log.appFileURL - appManager.logFormat = Constants.Log.appLogFormat - appManager.tunnelLogFormat = Constants.Log.tunnelLogFormat - appManager.configureLogging() - pp_log.info("Logging to: \(appManager.logFile!)") + let store = UserDefaultsStore(defaults: .standard) - let persistenceManager = PersistenceManager(author: appManager.persistenceAuthor) + logManager = LogManager(logFile: Constants.Log.appFileURL) + logManager.logLevel = Constants.Log.logLevel + logManager.logFormat = Constants.Log.appLogFormat + logManager.configureLogging() + pp_log.info("Logging to: \(logManager.logFile!)") + + let persistenceManager = PersistenceManager(store: store) profilesPersistence = persistenceManager.profilesPersistence( withName: Constants.Persistence.profilesContainerName ) @@ -81,6 +80,8 @@ class AppContext { withName: Constants.Persistence.providersContainerName ) + upgradeManager = UpgradeManager(store: store) + providerManager = ProviderManager( appBuild: Constants.Global.appBuildNumber, bundleServices: DefaultWebServices.bundledServices( @@ -95,6 +96,7 @@ class AppContext { ) profileManager = ProfileManager( + store: store, providerManager: providerManager, appGroup: Constants.App.appGroupId, keychainLabel: Unlocalized.Keychain.passwordLabel, @@ -112,7 +114,7 @@ class AppContext { ) #endif vpnManager = VPNManager( - appManager: appManager, + store: store, profileManager: profileManager, providerManager: providerManager, strategy: strategy @@ -137,14 +139,11 @@ class AppContext { // core providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager + vpnManager.tunnelLogFormat = Constants.Log.tunnelLogFormat vpnManager.isOnDemandRulesSupported = { self.isEligibleForOnDemandRules() } - if let activeProfileId = appManager.activeProfileId { - profileManager.setActiveProfileId(activeProfileId) - } - profileManager.observeUpdates() vpnManager.observeUpdates() @@ -180,8 +179,8 @@ class AppContext { } } -extension AppManager { - static let shared = AppContext.shared.appManager +extension UpgradeManager { + static let shared = AppContext.shared.upgradeManager } extension ProfileManager { diff --git a/Passepartout/App/Constants/AppPreference.swift b/Passepartout/App/Constants/AppPreference.swift new file mode 100644 index 00000000..916a2b72 --- /dev/null +++ b/Passepartout/App/Constants/AppPreference.swift @@ -0,0 +1,37 @@ +// +// AppPreference.swift +// Passepartout +// +// Created by Davide De Rosa on 6/15/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation +import PassepartoutUtils + +enum AppPreference: String, KeyStoreDomainLocation { + case isShowingFavorites + + case didHandleSubreddit + + var domain: String { + "App" + } +} diff --git a/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift b/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift index 4e6f07d4..39356a9e 100644 --- a/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift +++ b/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift @@ -37,8 +37,6 @@ extension DiagnosticsView { } } - @ObservedObject private var appManager: AppManager - @ObservedObject private var providerManager: ProviderManager @ObservedObject private var vpnManager: VPNManager @@ -62,7 +60,6 @@ extension DiagnosticsView { private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval init(providerName: ProviderName?) { - appManager = .shared providerManager = .shared vpnManager = .shared currentVPNState = .shared @@ -120,7 +117,7 @@ extension DiagnosticsView { ) } }.disabled(url == nil) - Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $appManager.masksPrivateData) + Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $vpnManager.masksPrivateData) } footer: { Text(L10n.Diagnostics.Sections.DebugLog.footer) } diff --git a/Passepartout/App/Views/OrganizerView+Profiles.swift b/Passepartout/App/Views/OrganizerView+Profiles.swift index 30a2e891..9dc32bc4 100644 --- a/Passepartout/App/Views/OrganizerView+Profiles.swift +++ b/Passepartout/App/Views/OrganizerView+Profiles.swift @@ -142,7 +142,7 @@ extension OrganizerView { private func performMigrationsIfNeeded() { Task { - AppManager.shared.doMigrations(profileManager) + UpgradeManager.shared.doMigrations(profileManager) } } } diff --git a/Passepartout/App/Views/OrganizerView.swift b/Passepartout/App/Views/OrganizerView.swift index 23be8485..d204f5d4 100644 --- a/Passepartout/App/Views/OrganizerView.swift +++ b/Passepartout/App/Views/OrganizerView.swift @@ -48,7 +48,7 @@ struct OrganizerView: View { @State private var isHostFileImporterPresented = false - @AppStorage(AppManager.DefaultKey.didHandleSubreddit.rawValue) var didHandleSubreddit = false + @AppStorage(AppPreference.didHandleSubreddit.key) private var didHandleSubreddit = false private let hostFileTypes = Constants.URLs.filetypes diff --git a/Passepartout/App/Views/ProviderLocationView.swift b/Passepartout/App/Views/ProviderLocationView.swift index f4cd25df..29da3ef1 100644 --- a/Passepartout/App/Views/ProviderLocationView.swift +++ b/Passepartout/App/Views/ProviderLocationView.swift @@ -29,8 +29,6 @@ import PassepartoutCore struct ProviderLocationView: View, ProviderProfileAvailability { @ObservedObject var providerManager: ProviderManager - @ObservedObject private var appManager: AppManager - @ObservedObject private var currentProfile: ObservableProfile var profile: Profile { @@ -55,7 +53,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability { @Binding private var favoriteLocationIds: Set? - @AppStorage(AppManager.DefaultKey.isShowingFavorites.rawValue) private var isShowingFavorites = false + @AppStorage(AppPreference.isShowingFavorites.key) private var isShowingFavorites = false private var isShowingEmptyFavorites: Bool { guard isShowingFavorites else { @@ -68,7 +66,6 @@ struct ProviderLocationView: View, ProviderProfileAvailability { init(currentProfile: ObservableProfile, isEditable: Bool, isPresented: Binding) { let providerManager: ProviderManager = .shared - appManager = .shared self.providerManager = providerManager self.currentProfile = currentProfile self.isEditable = isEditable diff --git a/Passepartout/App/Views/VPNToggle.swift b/Passepartout/App/Views/VPNToggle.swift index 29579a62..49e75c2a 100644 --- a/Passepartout/App/Views/VPNToggle.swift +++ b/Passepartout/App/Views/VPNToggle.swift @@ -27,8 +27,6 @@ import SwiftUI import PassepartoutCore struct VPNToggle: View { - @ObservedObject private var appManager: AppManager - @ObservedObject private var profileManager: ProfileManager @ObservedObject private var vpnManager: VPNManager @@ -64,7 +62,6 @@ struct VPNToggle: View { @State private var canToggle = true init(profileId: UUID, rateLimit: Int) { - appManager = .shared profileManager = .shared vpnManager = .shared currentVPNState = .shared @@ -87,10 +84,6 @@ struct VPNToggle: View { } do { let profile = try await vpnManager.connect(with: profileId) - - // IMPORTANT: save immediately to keep in sync with VPN status - appManager.activeProfileId = profileId - donateIntents(withProfile: profile) } catch { pp_log.warning("Unable to connect to profile \(profileId): \(error)") diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/AppManager.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/AppManager.swift deleted file mode 100644 index a355bc46..00000000 --- a/PassepartoutCore/Sources/PassepartoutCore/Managers/AppManager.swift +++ /dev/null @@ -1,221 +0,0 @@ -// -// AppManager.swift -// Passepartout -// -// Created by Davide De Rosa on 2/8/22. -// Copyright (c) 2022 Davide De Rosa. All rights reserved. -// -// https://github.com/passepartoutvpn -// -// This file is part of Passepartout. -// -// Passepartout is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Passepartout is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Passepartout. If not, see . -// - -import Foundation -import CoreData -import SwiftyBeaver - -@MainActor -public class AppManager: ObservableObject { - public enum DefaultKey: String { - case activeProfileId - - case launchesOnLogin - - case isShowingFavorites - - case confirmsQuit - - case logFormat - - case tunnelLogFormat - - case masksPrivateData - - case didHandleSubreddit - - // internal use - - case persistenceAuthor - - case didMigrateToV2 - } - - private let defaults: UserDefaults = .standard - - public var logLevel: SwiftyBeaver.Level = .info - - public var logFile: URL? - - // MARK: State - - @Published public private(set) var isDoingMigrations = true - - public init() { - defaults.register(keyedDefaults: [ - .activeProfileId: nil, - .launchesOnLogin: false, - .isShowingFavorites: false, - .confirmsQuit: true, - .logFormat: nil, - .tunnelLogFormat: nil, - .masksPrivateData: true, - .didHandleSubreddit: false, - // - .didMigrateToV2: false - ]) - - // set once - if persistenceAuthor == nil { - persistenceAuthor = UUID().uuidString - } - } - - public func configureLogging() { - let console = ConsoleDestination() - console.minLevel = logLevel -// console.useNSLog = true - if let logFormat = logFormat { - console.format = logFormat - } - SwiftyBeaver.addDestination(console) - - if let fileURL = logFile { - let file = FileDestination() - file.minLevel = logLevel - file.logFileURL = fileURL - if let logFormat = logFormat { - file.format = logFormat - } - _ = file.deleteLogFile() - SwiftyBeaver.addDestination(file) - } - - CoreConfiguration.masksPrivateData = masksPrivateData - } - - public func doMigrations(_ profileManager: ProfileManager) { -// profileManager.removeAllProfiles() - guard didMigrateToV2 else { - isDoingMigrations = true - let migrated = doMigrateToV2() - if !migrated.isEmpty { - pp_log.info("Migrating \(migrated.count) profiles") - migrated.forEach { - var profile = $0 - if profileManager.isExistingProfile(withName: profile.header.name) { - profile = profile.renamedUniquely(withLastUpdate: true) - } - profileManager.saveProfile(profile, isActive: nil) - } - } else { - pp_log.info("Nothing to migrate!") - } - isDoingMigrations = false - - didMigrateToV2 = true - return - } - isDoingMigrations = false - } - - // MARK: Current state - - public var preferences: AppPreferences { - return DefaultAppPreferences( - activeProfileId: activeProfileId, - logFormat: logFormat, - tunnelLogFormat: tunnelLogFormat, - masksPrivateData: masksPrivateData - ) - } -} - -extension AppManager: AppPreferences { - public var activeProfileId: UUID? { - get { - guard let uuidString = defaults.string(forKey: DefaultKey.activeProfileId.rawValue) else { - return nil - } - return UUID(uuidString: uuidString) - } - set { - defaults.set(newValue?.uuidString, forKey: DefaultKey.activeProfileId.rawValue) - defaults.synchronize() - objectWillChange.send() - } - } - - public var logFormat: String? { - get { - defaults.string(forKey: DefaultKey.logFormat.rawValue) - } - set { - defaults.set(newValue, forKey: DefaultKey.logFormat.rawValue) - objectWillChange.send() - } - } - - public var tunnelLogFormat: String? { - get { - defaults.string(forKey: DefaultKey.tunnelLogFormat.rawValue) - } - set { - defaults.set(newValue, forKey: DefaultKey.tunnelLogFormat.rawValue) - objectWillChange.send() - } - } - - public var masksPrivateData: Bool { - get { - defaults.bool(forKey: DefaultKey.masksPrivateData.rawValue) - } - set { - defaults.set(newValue, forKey: DefaultKey.masksPrivateData.rawValue) - CoreConfiguration.masksPrivateData = newValue - - objectWillChange.send() - } - } - - // MARK: Internal use (readonly) - - public private(set) var persistenceAuthor: String? { - get { - defaults.string(forKey: DefaultKey.persistenceAuthor.rawValue) - } - set { - defaults.set(newValue, forKey: DefaultKey.persistenceAuthor.rawValue) - } - } - - public internal(set) var didMigrateToV2: Bool { - get { - defaults.bool(forKey: DefaultKey.didMigrateToV2.rawValue) - } - set { - defaults.set(newValue, forKey: DefaultKey.didMigrateToV2.rawValue) - } - } -} - -private extension UserDefaults { - func register(keyedDefaults: [AppManager.DefaultKey: Any?]) { - let mapped = keyedDefaults.reduce(into: [String: Any]()) { - $0[$1.key.rawValue] = $1.value - } - register(defaults: mapped) - } -} diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/LogManager.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/LogManager.swift new file mode 100644 index 00000000..9d999af0 --- /dev/null +++ b/PassepartoutCore/Sources/PassepartoutCore/Managers/LogManager.swift @@ -0,0 +1,60 @@ +// +// LogManager.swift +// Passepartout +// +// Created by Davide De Rosa on 6/15/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation +import SwiftyBeaver + +public class LogManager { + public let logFile: URL? + + public var logLevel: SwiftyBeaver.Level = .info + + public var logFormat: String? + + public init(logFile: URL?) { + self.logFile = logFile + } + + public func configureLogging() { + let console = ConsoleDestination() + console.minLevel = logLevel +// console.useNSLog = true + if let logFormat = logFormat { + console.format = logFormat + } + SwiftyBeaver.addDestination(console) + + if let fileURL = logFile { + let file = FileDestination() + file.minLevel = logLevel + file.logFileURL = fileURL + if let logFormat = logFormat { + file.format = logFormat + } + _ = file.deleteLogFile() + SwiftyBeaver.addDestination(file) + } + } +} diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/PersistenceManager.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/PersistenceManager.swift index 2af5f8a4..81d93d79 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Managers/PersistenceManager.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Managers/PersistenceManager.swift @@ -25,21 +25,50 @@ import Foundation import CoreData +import PassepartoutUtils public class PersistenceManager { - private let author: String? + private let store: KeyValueStore - public init(author: String?) { - self.author = author + public init(store: KeyValueStore) { + self.store = store + + // set once + if persistenceAuthor == nil { + persistenceAuthor = UUID().uuidString + } } public func profilesPersistence(withName containerName: String) -> Persistence { let model = PassepartoutDataModels.profiles - return Persistence(withCloudKitName: containerName, model: model, author: author) + return Persistence(withCloudKitName: containerName, model: model, author: persistenceAuthor) } public func providersPersistence(withName containerName: String) -> Persistence { let model = PassepartoutDataModels.providers - return Persistence(withLocalName: containerName, model: model, author: author) + return Persistence(withLocalName: containerName, model: model, author: persistenceAuthor) + } +} + +// MARK: KeyValueStore + +extension PersistenceManager { + public private(set) var persistenceAuthor: String? { + get { + store.value(forLocation: StoreKey.persistenceAuthor) + } + set { + store.setValue(newValue, forLocation: StoreKey.persistenceAuthor) + } + } +} + +private extension PersistenceManager { + private enum StoreKey: String, KeyStoreDomainLocation { + case persistenceAuthor + + var domain: String { + "PersistenceManager" + } } } diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/AppManager+Migrations.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/UpgradeManager+Migrations.swift similarity index 94% rename from PassepartoutCore/Sources/PassepartoutCore/Managers/AppManager+Migrations.swift rename to PassepartoutCore/Sources/PassepartoutCore/Managers/UpgradeManager+Migrations.swift index e6895fe1..06cd4a42 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Managers/AppManager+Migrations.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Managers/UpgradeManager+Migrations.swift @@ -1,5 +1,5 @@ // -// AppManager+Migrations.swift +// UpgradeManager+Migrations.swift // Passepartout // // Created by Davide De Rosa on 3/20/22. @@ -28,12 +28,34 @@ import GenericJSON import TunnelKitCore import TunnelKitOpenVPNCore import TunnelKitManager +import PassepartoutUtils private typealias Map = [String: Any] +// MARK: Migrate old store + +extension UpgradeManager { + fileprivate enum LegacyStoreKey: String, KeyStoreLocation { + case didMigrateToV2 + + var key: String { + rawValue + } + } + + func doMigrateStore(_ store: KeyValueStore) { + if !didMigrateToV2 { + guard let legacyDidMigrateToV2: Bool = store.value(forLocation: LegacyStoreKey.didMigrateToV2) else { + return + } + didMigrateToV2 = legacyDidMigrateToV2 + } + } +} + // MARK: Migrate to version 2 -extension AppManager { +extension UpgradeManager { fileprivate enum MigrationError: Error { case json @@ -321,7 +343,7 @@ private extension URL { func asJSON() throws -> Map { let data = try Data(contentsOf: self) guard let json = try JSONSerialization.jsonObject(with: data) as? Map else { - throw AppManager.MigrationError.json + throw UpgradeManager.MigrationError.json } return json } diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/UpgradeManager.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/UpgradeManager.swift new file mode 100644 index 00000000..220b2854 --- /dev/null +++ b/PassepartoutCore/Sources/PassepartoutCore/Managers/UpgradeManager.swift @@ -0,0 +1,95 @@ +// +// UpgradeManager.swift +// Passepartout +// +// Created by Davide De Rosa on 2/8/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation +import CoreData +import SwiftyBeaver +import PassepartoutUtils + +@MainActor +public class UpgradeManager: ObservableObject { + + // MARK: Initialization + + private let store: KeyValueStore + + // MARK: State + + @Published public private(set) var isDoingMigrations = true + + public init(store: KeyValueStore) { + self.store = store + } + + public func doMigrations(_ profileManager: ProfileManager) { + doMigrateStore(store) + +// profileManager.removeAllProfiles() + guard didMigrateToV2 else { + isDoingMigrations = true + let migrated = doMigrateToV2() + if !migrated.isEmpty { + pp_log.info("Migrating \(migrated.count) profiles") + migrated.forEach { + var profile = $0 + if profileManager.isExistingProfile(withName: profile.header.name) { + profile = profile.renamedUniquely(withLastUpdate: true) + } + profileManager.saveProfile(profile, isActive: nil) + } + } else { + pp_log.info("Nothing to migrate!") + } + isDoingMigrations = false + + didMigrateToV2 = true + return + } + isDoingMigrations = false + } +} + +// MARK: KeyValueStore + +extension UpgradeManager { + public internal(set) var didMigrateToV2: Bool { + get { + store.value(forLocation: StoreKey.didMigrateToV2) ?? false + } + set { + store.setValue(newValue, forLocation: StoreKey.didMigrateToV2) + } + } +} + +private extension UpgradeManager { + private enum StoreKey: String, KeyStoreDomainLocation { + case didMigrateToV2 + + var domain: String { + "UpgradeManager" + } + } +} diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager+Configuration.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager+Configuration.swift index 1fe6196e..e0921ab2 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager+Configuration.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager+Configuration.swift @@ -26,6 +26,13 @@ import Foundation extension VPNManager { + private var vpnPreferences: VPNPreferences { + DefaultVPNPreferences( + tunnelLogFormat: tunnelLogFormat, + masksPrivateData: masksPrivateData + ) + } + func vpnConfigurationWithCurrentProfile() -> VPNConfiguration? { do { guard profileManager.isCurrentProfileActive() else { @@ -52,7 +59,7 @@ extension VPNManager { let parameters = VPNConfigurationParameters( profile, appGroup: profileManager.appGroup, - preferences: appManager.preferences, + preferences: vpnPreferences, passwordReference: profileManager.passwordReference(forProfile: profile), withNetworkSettings: isNetworkSettingsSupported(), withCustomRules: isOnDemandRulesSupported() diff --git a/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager.swift b/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager.swift index 52599fa0..69594fd3 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Managers/VPNManager.swift @@ -25,8 +25,9 @@ import Foundation import Combine +import TunnelKitCore import TunnelKitManager -import TunnelKitOpenVPNManager +import PassepartoutUtils @MainActor public class VPNManager: ObservableObject { @@ -34,8 +35,8 @@ public class VPNManager: ObservableObject { // MARK: Initialization - let appManager: AppManager - + private let store: KeyValueStore + let profileManager: ProfileManager let providerManager: ProviderManager @@ -68,12 +69,12 @@ public class VPNManager: ObservableObject { private var cancellables: Set = [] public init( - appManager: AppManager, + store: KeyValueStore, profileManager: ProfileManager, providerManager: ProviderManager, strategy: VPNManagerStrategy ) { - self.appManager = appManager + self.store = store self.profileManager = profileManager self.providerManager = providerManager self.strategy = strategy @@ -81,6 +82,8 @@ public class VPNManager: ObservableObject { isOnDemandRulesSupported = { true } currentState = ObservableState() + + CoreConfiguration.masksPrivateData = masksPrivateData } public func toggle() -> Bool { @@ -152,7 +155,7 @@ extension VPNManager { } private func observeProfileManager() { - profileManager.$activeProfileId + profileManager.activeProfileIdPublisher .dropFirst() .removeDuplicates() .sink { newId in @@ -252,3 +255,39 @@ extension VPNManager { } } } + +// MARK: KeyValueStore + +extension VPNManager { + public var tunnelLogFormat: String? { + get { + store.value(forLocation: StoreKey.tunnelLogFormat) + } + set { + store.setValue(newValue, forLocation: StoreKey.tunnelLogFormat) + } + } + + public var masksPrivateData: Bool { + get { + store.value(forLocation: StoreKey.masksPrivateData) ?? true + } + set { + store.setValue(newValue, forLocation: StoreKey.masksPrivateData) + + CoreConfiguration.masksPrivateData = masksPrivateData + } + } +} + +private extension VPNManager { + private enum StoreKey: String, KeyStoreDomainLocation { + case tunnelLogFormat + + case masksPrivateData + + var domain: String { + "VPNManager" + } + } +} diff --git a/PassepartoutCore/Sources/PassepartoutCore/Models/VPNConfiguration.swift b/PassepartoutCore/Sources/PassepartoutCore/Models/VPNConfiguration.swift index 8c225b0c..6877b88c 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Models/VPNConfiguration.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Models/VPNConfiguration.swift @@ -39,7 +39,7 @@ struct VPNConfigurationParameters { let appGroup: String - let preferences: AppPreferences + let preferences: VPNPreferences let networkSettings: Profile.NetworkSettings @@ -54,7 +54,7 @@ struct VPNConfigurationParameters { init( _ profile: Profile, appGroup: String, - preferences: AppPreferences, + preferences: VPNPreferences, passwordReference: Data?, withNetworkSettings: Bool, withCustomRules: Bool diff --git a/PassepartoutCore/Sources/PassepartoutCore/Models/AppPreferences.swift b/PassepartoutCore/Sources/PassepartoutCore/Models/VPNPreferences.swift similarity index 70% rename from PassepartoutCore/Sources/PassepartoutCore/Models/AppPreferences.swift rename to PassepartoutCore/Sources/PassepartoutCore/Models/VPNPreferences.swift index 1641a504..90a7ee0e 100644 --- a/PassepartoutCore/Sources/PassepartoutCore/Models/AppPreferences.swift +++ b/PassepartoutCore/Sources/PassepartoutCore/Models/VPNPreferences.swift @@ -1,8 +1,8 @@ // -// AppPreferences.swift +// VPNPreferences.swift // Passepartout // -// Created by Davide De Rosa on 2/10/22. +// Created by Davide De Rosa on 6/6/22. // Copyright (c) 2022 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -25,23 +25,14 @@ import Foundation -@MainActor -public protocol AppPreferences { - var activeProfileId: UUID? { get } - - var logFormat: String? { get } - +public protocol VPNPreferences { var tunnelLogFormat: String? { get } var masksPrivateData: Bool { get } } -public struct DefaultAppPreferences: AppPreferences { - public let activeProfileId: UUID? +struct DefaultVPNPreferences: VPNPreferences { + let tunnelLogFormat: String? - public let logFormat: String? - - public let tunnelLogFormat: String? - - public let masksPrivateData: Bool + let masksPrivateData: Bool } diff --git a/PassepartoutCore/Sources/PassepartoutProfiles/Managers/ProfileManager.swift b/PassepartoutCore/Sources/PassepartoutProfiles/Managers/ProfileManager.swift index 34c82789..961c4fee 100644 --- a/PassepartoutCore/Sources/PassepartoutProfiles/Managers/ProfileManager.swift +++ b/PassepartoutCore/Sources/PassepartoutProfiles/Managers/ProfileManager.swift @@ -35,6 +35,8 @@ public class ProfileManager: ObservableObject { // MARK: Initialization + private let store: KeyValueStore + private let providerManager: ProviderManager public let appGroup: String @@ -47,7 +49,7 @@ public class ProfileManager: ObservableObject { // MARK: Observables - @Published public private(set) var activeProfileId: UUID? { + @Published private var internalActiveProfileId: UUID? { willSet { pp_log.debug("Setting active profile: \(newValue?.uuidString ?? "nil")") } @@ -59,6 +61,10 @@ public class ProfileManager: ObservableObject { } } + public var activeProfileIdPublisher: Published.Publisher { + $internalActiveProfileId + } + public var currentProfileId: UUID? { get { internalCurrentProfileId @@ -83,6 +89,7 @@ public class ProfileManager: ObservableObject { private var cancellables: Set = [] public init( + store: KeyValueStore, providerManager: ProviderManager, appGroup: String, keychainLabel: @escaping (String, VPNProtocolType) -> String, @@ -91,6 +98,7 @@ public class ProfileManager: ObservableObject { guard let _ = UserDefaults(suiteName: appGroup) else { fatalError("No entitlements for group '\(appGroup)'") } + self.store = store self.providerManager = providerManager self.appGroup = appGroup self.keychainLabel = keychainLabel @@ -99,14 +107,6 @@ public class ProfileManager: ObservableObject { currentProfile = ObservableProfile() } - - public func setActiveProfileId(_ id: UUID) { - guard isExistingProfile(withId: id) else { - pp_log.warning("Active profile \(id) does not exist, ignoring") - return - } - activeProfileId = id - } } // MARK: Index @@ -450,3 +450,41 @@ extension ProfileManager { } } } + +// MARK: KeyValueStore + +extension ProfileManager { + public private(set) var activeProfileId: UUID? { + get { + guard let idString: String = store.value(forLocation: StoreKey.activeProfileId) else { + return nil + } + guard let id = UUID(uuidString: idString) else { + pp_log.warning("Active profile id is malformed, ignoring") + return nil + } + guard isExistingProfile(withId: id) else { + pp_log.warning("Active profile \(id) does not exist, ignoring") + return nil + } + return id + } + set { + + // trigger publisher + internalActiveProfileId = newValue + + store.setValue(newValue?.uuidString, forLocation: StoreKey.activeProfileId) + } + } +} + +private extension ProfileManager { + private enum StoreKey: String, KeyStoreDomainLocation { + case activeProfileId + + var domain: String { + "ProfileManager" + } + } +} diff --git a/PassepartoutCore/Sources/PassepartoutUtils/Models/KeyValueStore.swift b/PassepartoutCore/Sources/PassepartoutUtils/Models/KeyValueStore.swift new file mode 100644 index 00000000..652659d1 --- /dev/null +++ b/PassepartoutCore/Sources/PassepartoutUtils/Models/KeyValueStore.swift @@ -0,0 +1,46 @@ +// +// KeyValueStore.swift +// Passepartout +// +// Created by Davide De Rosa on 6/15/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation + +public protocol KeyStoreLocation: RawRepresentable where RawValue == String { + var key: String { get } +} + +public protocol KeyStoreDomainLocation: KeyStoreLocation { + var domain: String { get } +} + +extension KeyStoreDomainLocation { + public var key: String { + "\(domain).\(rawValue)" + } +} + +public protocol KeyValueStore { + func setValue(_ value: V, forLocation location: L) + + func value(forLocation location: L) -> V? +} diff --git a/PassepartoutCore/Sources/PassepartoutUtils/Models/UserDefaultsStore.swift b/PassepartoutCore/Sources/PassepartoutUtils/Models/UserDefaultsStore.swift new file mode 100644 index 00000000..941b831c --- /dev/null +++ b/PassepartoutCore/Sources/PassepartoutUtils/Models/UserDefaultsStore.swift @@ -0,0 +1,42 @@ +// +// UserDefaultsStore.swift +// Passepartout +// +// Created by Davide De Rosa on 6/15/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation + +public struct UserDefaultsStore: KeyValueStore { + private let defaults: UserDefaults + + public init(defaults: UserDefaults) { + self.defaults = defaults + } + + public func setValue(_ value: V, forLocation location: L) { + defaults.setValue(value, forKey: location.key) + } + + public func value(forLocation location: L) -> V? { + defaults.value(forKey: location.key) as? V + } +}