diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-11-09 13:57:29 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-11-09 13:57:29 +0100 |
| commit | af24fbacb6b85f93286e28cea8aefae7c698bf07 (patch) | |
| tree | 1c62114b50ccecfc8d686e33a2840d75bce9be23 | |
| parent | 2af40170885057f6fa6a40ffb071d2b998265a54 (diff) | |
| parent | fcc639078249f8da58c48e2cb3c261dd8a754c84 (diff) | |
| download | mullvadvpn-af24fbacb6b85f93286e28cea8aefae7c698bf07.tar.xz mullvadvpn-af24fbacb6b85f93286e28cea8aefae7c698bf07.zip | |
Merge branch 'no-shared-instances'
21 files changed, 253 insertions, 169 deletions
diff --git a/ios/MullvadREST/AddressCache.swift b/ios/MullvadREST/AddressCache.swift index 647c495314..c18709f0ef 100644 --- a/ios/MullvadREST/AddressCache.swift +++ b/ios/MullvadREST/AddressCache.swift @@ -12,11 +12,6 @@ import MullvadTypes extension REST { public final class AddressCache { - public static let shared = AddressCache( - securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier, - isReadOnly: false - )! - /// Logger. private let logger = Logger(label: "AddressCache") diff --git a/ios/MullvadREST/Info.plist b/ios/MullvadREST/Info.plist index 65663845f1..0c67376eba 100644 --- a/ios/MullvadREST/Info.plist +++ b/ios/MullvadREST/Info.plist @@ -1,8 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> -<dict> - <key>ApplicationSecurityGroupIdentifier</key> - <string>$(SECURITY_GROUP_IDENTIFIER)</string> -</dict> +<dict/> </plist> diff --git a/ios/MullvadREST/RESTProxyFactory.swift b/ios/MullvadREST/RESTProxyFactory.swift index f1b7d74cc0..211d32b9aa 100644 --- a/ios/MullvadREST/RESTProxyFactory.swift +++ b/ios/MullvadREST/RESTProxyFactory.swift @@ -12,25 +12,29 @@ extension REST { public final class ProxyFactory { public let configuration: AuthProxyConfiguration - public static let shared: ProxyFactory = { - let basicConfiguration = ProxyConfiguration( - transportRegistry: TransportRegistry.shared, - addressCacheStore: AddressCache.shared + public class func makeProxyFactory( + transportRegistry: REST.TransportRegistry, + addressCache: AddressCache + ) -> ProxyFactory { + let basicConfiguration = REST.ProxyConfiguration( + transportRegistry: transportRegistry, + addressCacheStore: addressCache ) let authenticationProxy = REST.AuthenticationProxy( configuration: basicConfiguration ) - let accessTokenManager = AccessTokenManager( + let accessTokenManager = REST.AccessTokenManager( authenticationProxy: authenticationProxy ) - let authConfiguration = AuthProxyConfiguration( + let authConfiguration = REST.AuthProxyConfiguration( proxyConfiguration: basicConfiguration, accessTokenManager: accessTokenManager ) + return ProxyFactory(configuration: authConfiguration) - }() + } public init(configuration: AuthProxyConfiguration) { self.configuration = configuration diff --git a/ios/MullvadREST/RESTTransportRegistry.swift b/ios/MullvadREST/RESTTransportRegistry.swift index b41545fbba..1dcac36962 100644 --- a/ios/MullvadREST/RESTTransportRegistry.swift +++ b/ios/MullvadREST/RESTTransportRegistry.swift @@ -10,12 +10,12 @@ import Foundation extension REST { public final class TransportRegistry { - public static let shared = TransportRegistry() - private var transport: RESTTransport? private let nslock = NSLock() - private init() {} + public init(transport: RESTTransport?) { + self.transport = transport + } public func setTransport(_ transport: RESTTransport) { nslock.lock() diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 3e7b6261aa..9f6bd376f1 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -126,7 +126,6 @@ 584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */; }; 584F99202902CBDD001F858D /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; }; 584F99212902CF35001F858D /* libMullvadTypes.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 581943F128F8014500B0CB5E /* libMullvadTypes.a */; }; - 58505FFA290A7F0F00118C23 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 5856AD582902BE1A008E5127 /* PacketTunnelRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */; }; 5856AD592902BE1A008E5127 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; }; 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */; }; @@ -1117,13 +1116,13 @@ name = Frameworks; sourceTree = "<group>"; }; - 585DA87526B0249A00B8C587 /* RelayCache */ = { + 585DA87526B0249A00B8C587 /* RelayCacheTracker */ = { isa = PBXGroup; children = ( 58FB865926EA214400F188BC /* RelayCacheTrackerObserver.swift */, 58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */, ); - path = RelayCache; + path = RelayCacheTracker; sourceTree = "<group>"; }; 586A950B2901250A007BAF2B /* Operations */ = { @@ -1332,8 +1331,8 @@ 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */, 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */, 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */, - 585DA87526B0249A00B8C587 /* RelayCache */, 5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */, + 585DA87526B0249A00B8C587 /* RelayCacheTracker */, 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */, 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */, 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */, @@ -2027,7 +2026,6 @@ 06799AF128F98E4800ACD94E /* RESTAPIProxy.swift in Sources */, 06799AED28F98E4800ACD94E /* RESTTransportRegistry.swift in Sources */, 06799AE528F98E4800ACD94E /* HTTP.swift in Sources */, - 58505FFA290A7F0F00118C23 /* ApplicationConfiguration.swift in Sources */, 06799AE028F98E4800ACD94E /* RESTCoding.swift in Sources */, 06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */, 5897F1762914E62E00AF5695 /* Duration.swift in Sources */, @@ -2214,8 +2212,8 @@ 5878A27729093A4F0096FC88 /* StorePaymentBlockObserver.swift in Sources */, 5868585524054096000B8131 /* AppButton.swift in Sources */, 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */, - 585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */, 5867771629097C5B006F721F /* ProductState.swift in Sources */, + 585E820327F3285E00939F0E /* SendStoreReceiptOperation.swift in Sources */, 584B17AB27637DE40057F3B8 /* ReconnectTunnelOperation.swift in Sources */, 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */, 585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */, diff --git a/ios/MullvadVPN/AccountDataThrottling.swift b/ios/MullvadVPN/AccountDataThrottling.swift index 31f0b1f849..f6093beeb2 100644 --- a/ios/MullvadVPN/AccountDataThrottling.swift +++ b/ios/MullvadVPN/AccountDataThrottling.swift @@ -30,7 +30,7 @@ struct AccountDataThrottling { let tunnelManager: TunnelManager private(set) var lastUpdate: Date? - init(tunnelManager: TunnelManager = .shared) { + init(tunnelManager: TunnelManager) { self.tunnelManager = tunnelManager } diff --git a/ios/MullvadVPN/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCacheTracker.swift index 1f90030f1f..ee19bc1431 100644 --- a/ios/MullvadVPN/AddressCacheTracker.swift +++ b/ios/MullvadVPN/AddressCacheTracker.swift @@ -6,19 +6,13 @@ // Copyright © 2021 Mullvad VPN AB. All rights reserved. // -import Foundation import MullvadLogging import MullvadREST import MullvadTypes import Operations +import UIKit final class AddressCacheTracker { - /// Shared instance. - static let shared = AddressCacheTracker( - apiProxy: REST.ProxyFactory.shared.createAPIProxy(), - store: REST.AddressCache.shared - ) - /// Update interval (in seconds). private static let updateInterval: TimeInterval = 60 * 60 * 24 @@ -27,6 +21,7 @@ final class AddressCacheTracker { /// Logger. private let logger = Logger(label: "AddressCache.Tracker") + private let application: UIApplication /// REST API proxy. private let apiProxy: REST.APIProxy @@ -54,7 +49,8 @@ final class AddressCacheTracker { private let nslock = NSLock() /// Designated initializer - private init(apiProxy: REST.APIProxy, store: REST.AddressCache) { + init(application: UIApplication, apiProxy: REST.APIProxy, store: REST.AddressCache) { + self.application = application self.apiProxy = apiProxy self.store = store } @@ -120,7 +116,7 @@ final class AddressCacheTracker { operation.addObserver( BackgroundObserver( - application: .shared, + application: application, name: "Update endpoints", cancelUponExpiration: true ) diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 34367b96e1..1d5141bc61 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -32,7 +32,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return operationQueue }() - private let transportMonitor = TransportMonitor() + private(set) var tunnelManager: TunnelManager! + private(set) var addressCache: REST.AddressCache! + + private var proxyFactory: REST.ProxyFactory! + private(set) var apiProxy: REST.APIProxy! + private(set) var accountsProxy: REST.AccountsProxy! + private(set) var devicesProxy: REST.DevicesProxy! + + private(set) var addressCacheTracker: AddressCacheTracker! + private(set) var relayCacheTracker: RelayCacheTracker! + private(set) var storePaymentManager: StorePaymentManager! + private var transportMonitor: TransportMonitor! // MARK: - Application lifecycle @@ -47,10 +58,51 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD logger = Logger(label: "AppDelegate") + addressCache = REST.AddressCache( + securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier, + isReadOnly: false + )! + + let transportRegistry = REST.TransportRegistry(transport: nil) + proxyFactory = REST.ProxyFactory.makeProxyFactory( + transportRegistry: transportRegistry, + addressCache: addressCache + ) + + apiProxy = proxyFactory.createAPIProxy() + accountsProxy = proxyFactory.createAccountsProxy() + devicesProxy = proxyFactory.createDevicesProxy() + + relayCacheTracker = RelayCacheTracker(application: application, apiProxy: apiProxy) + addressCacheTracker = AddressCacheTracker( + application: application, + apiProxy: apiProxy, + store: addressCache + ) + + tunnelManager = TunnelManager( + application: application, + relayCacheTracker: relayCacheTracker, + accountsProxy: accountsProxy, + devicesProxy: devicesProxy + ) + + storePaymentManager = StorePaymentManager( + application: application, + queue: .default(), + apiProxy: apiProxy, + accountsProxy: accountsProxy + ) + + transportMonitor = TransportMonitor( + tunnelManager: tunnelManager, + transportRegistry: transportRegistry + ) + #if targetEnvironment(simulator) // Configure mock tunnel provider on simulator simulatorTunnelProviderHost = SimulatorTunnelProviderHost( - relayCacheTracker: .shared + relayCacheTracker: relayCacheTracker ) SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost #endif @@ -61,7 +113,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD addApplicationNotifications(application: application) let setupTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in - TunnelManager.shared.loadConfiguration { error in + self.tunnelManager.loadConfiguration { error in // TODO: avoid throwing fatal error and show the problem report UI instead. if let error = error { fatalError(error.localizedDescription) @@ -70,7 +122,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.logger.debug("Finished initialization.") NotificationManager.shared.updateNotifications() - StorePaymentManager.shared.startPaymentQueueMonitoring() + self.storePaymentManager.startPaymentQueueMonitoring() operation.finish() } @@ -84,11 +136,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? { switch intent { case is StartVPNIntent: - return StartVPNIntentHandler(tunnelManager: .shared) + return StartVPNIntentHandler(tunnelManager: tunnelManager) case is StopVPNIntent: - return StopVPNIntentHandler(tunnelManager: .shared) + return StopVPNIntentHandler(tunnelManager: tunnelManager) case is ReconnectVPNIntent: - return ReconnectVPNIntentHandler(tunnelManager: .shared) + return ReconnectVPNIntentHandler(tunnelManager: tunnelManager) default: return nil } @@ -101,8 +153,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions ) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. let sceneConfiguration = UISceneConfiguration( name: "Default Configuration", sessionRole: connectingSceneSession.role @@ -115,27 +165,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application( _ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession> - ) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, - // this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to - // the discarded scenes, as they will not return. - } + ) {} // MARK: - Notifications @objc private func didBecomeActive(_ notification: Notification) { - TunnelManager.shared.refreshTunnelStatus() - TunnelManager.shared.startPeriodicPrivateKeyRotation() - RelayCacheTracker.shared.startPeriodicUpdates() - AddressCacheTracker.shared.startPeriodicUpdates() + tunnelManager.refreshTunnelStatus() + tunnelManager.startPeriodicPrivateKeyRotation() + relayCacheTracker.startPeriodicUpdates() + addressCacheTracker.startPeriodicUpdates() } @objc private func willResignActive(_ notification: Notification) { - TunnelManager.shared.stopPeriodicPrivateKeyRotation() - RelayCacheTracker.shared.stopPeriodicUpdates() - AddressCacheTracker.shared.stopPeriodicUpdates() + tunnelManager.stopPeriodicPrivateKeyRotation() + relayCacheTracker.stopPeriodicUpdates() + addressCacheTracker.stopPeriodicUpdates() } @objc private func didEnterBackground(_ notification: Notification) { @@ -155,7 +199,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD forTaskWithIdentifier: ApplicationConfiguration.appRefreshTaskIdentifier, using: nil ) { task in - let handle = RelayCacheTracker.shared.updateRelays { completion in + let handle = self.relayCacheTracker.updateRelays { completion in task.setTaskCompleted(success: completion.isSuccess) } @@ -178,7 +222,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD forTaskWithIdentifier: ApplicationConfiguration.privateKeyRotationTaskIdentifier, using: nil ) { task in - let handle = TunnelManager.shared.rotatePrivateKey(forceRotate: false) { completion in + let handle = self.tunnelManager.rotatePrivateKey(forceRotate: false) { completion in self.scheduleKeyRotationTask() task.setTaskCompleted(success: completion.isSuccess) @@ -201,7 +245,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD forTaskWithIdentifier: ApplicationConfiguration.addressCacheUpdateTaskIdentifier, using: nil ) { task in - let handle = AddressCacheTracker.shared.updateEndpoints { completion in + let handle = self.addressCacheTracker.updateEndpoints { completion in self.scheduleAddressCacheUpdateTask() task.setTaskCompleted(success: completion.isSuccess) @@ -227,7 +271,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func scheduleAppRefreshTask() { do { - let date = RelayCacheTracker.shared.getNextUpdateDate() + let date = relayCacheTracker.getNextUpdateDate() let request = BGAppRefreshTaskRequest( identifier: ApplicationConfiguration.appRefreshTaskIdentifier @@ -247,7 +291,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func scheduleKeyRotationTask() { do { - guard let date = TunnelManager.shared.getNextKeyRotationDate() else { + guard let date = tunnelManager.getNextKeyRotationDate() else { return } @@ -270,7 +314,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func scheduleAddressCacheUpdateTask() { do { - let date = AddressCacheTracker.shared.nextScheduleDate() + let date = addressCacheTracker.nextScheduleDate() let request = BGProcessingTaskRequest( identifier: ApplicationConfiguration.addressCacheUpdateTaskIdentifier @@ -315,14 +359,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } private func setupPaymentHandler() { - StorePaymentManager.shared.delegate = self - StorePaymentManager.shared.addPaymentObserver(TunnelManager.shared) + storePaymentManager.delegate = self + storePaymentManager.addPaymentObserver(tunnelManager) } private func setupNotificationHandler() { NotificationManager.shared.notificationProviders = [ - AccountExpiryNotificationProvider(), - TunnelStatusNotificationProvider(), + AccountExpiryNotificationProvider(tunnelManager: tunnelManager), + TunnelStatusNotificationProvider(tunnelManager: tunnelManager), ] UNUserNotificationCenter.current().delegate = self } @@ -336,7 +380,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Since we do not persist the relation between payment and account number between the // app launches, we assume that all successful purchases belong to the active account // number. - return TunnelManager.shared.deviceState.accountData?.number + return tunnelManager.deviceState.accountData?.number } // MARK: - UNUserNotificationCenterDelegate diff --git a/ios/MullvadVPN/DeviceManagementInteractor.swift b/ios/MullvadVPN/DeviceManagementInteractor.swift index 29ab2d1171..87ee7f204a 100644 --- a/ios/MullvadVPN/DeviceManagementInteractor.swift +++ b/ios/MullvadVPN/DeviceManagementInteractor.swift @@ -12,11 +12,12 @@ import MullvadTypes import Operations class DeviceManagementInteractor { - private let devicesProxy = REST.ProxyFactory.shared.createDevicesProxy() + private let devicesProxy: REST.DevicesProxy private let accountNumber: String - init(accountNumber: String) { + init(accountNumber: String, devicesProxy: REST.DevicesProxy) { self.accountNumber = accountNumber + self.devicesProxy = devicesProxy } @discardableResult diff --git a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift index 83cc888d82..fc6a579661 100644 --- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift @@ -10,7 +10,7 @@ import Foundation import UserNotifications let accountExpiryNotificationIdentifier = "net.mullvad.MullvadVPN.AccountExpiryNotification" -let accountExpiryDefaultTriggerInterval = 3 +private let defaultTriggerInterval = 3 class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificationProvider, InAppNotificationProvider, TunnelObserver @@ -24,13 +24,13 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio return accountExpiryNotificationIdentifier } - init(triggerInterval: Int = accountExpiryDefaultTriggerInterval) { + init(tunnelManager: TunnelManager, triggerInterval: Int = defaultTriggerInterval) { self.triggerInterval = triggerInterval super.init() - TunnelManager.shared.addObserver(self) - accountExpiry = TunnelManager.shared.deviceState.accountData?.expiry + tunnelManager.addObserver(self) + accountExpiry = tunnelManager.deviceState.accountData?.expiry } private var trigger: UNNotificationTrigger? { diff --git a/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift index 59642ccc0c..426d97dbad 100644 --- a/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/TunnelStatusNotificationProvider.swift @@ -31,10 +31,9 @@ class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationP } } - override init() { + init(tunnelManager: TunnelManager) { super.init() - let tunnelManager = TunnelManager.shared tunnelManager.addObserver(self) handleTunnelStatus(tunnelManager.tunnelStatus) } diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift index c6d0b64ef0..ef4bb33733 100644 --- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift @@ -14,7 +14,7 @@ import Operations import RelayCache import UIKit -class RelayCacheTracker { +final class RelayCacheTracker { /// Relay update interval (in seconds). static let relayUpdateInterval: TimeInterval = 60 * 60 @@ -26,6 +26,8 @@ class RelayCacheTracker { securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier )! + private let application: UIApplication + /// Lock used for synchronization. private let nslock = NSLock() @@ -43,7 +45,7 @@ class RelayCacheTracker { private var isPeriodicUpdatesEnabled = false /// API proxy. - private let apiProxy = REST.ProxyFactory.shared.createAPIProxy() + private let apiProxy: REST.APIProxy /// Observers. private let observerList = ObserverList<RelayCacheTrackerObserver>() @@ -51,10 +53,10 @@ class RelayCacheTracker { /// Memory cache. private var cachedRelays: CachedRelays? - /// A shared instance of `RelayCacheTracker`. - static let shared = RelayCacheTracker() + init(application: UIApplication, apiProxy: REST.APIProxy) { + self.application = application + self.apiProxy = apiProxy - private init() { do { cachedRelays = try cache.read() } catch { @@ -127,7 +129,7 @@ class RelayCacheTracker { operation.addObserver( BackgroundObserver( - application: .shared, + application: application, name: "Update relays", cancelUponExpiration: true ) diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTrackerObserver.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTrackerObserver.swift index fcb38b55d6..fcb38b55d6 100644 --- a/ios/MullvadVPN/RelayCache/RelayCacheTrackerObserver.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTrackerObserver.swift diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index dcecff3bab..27fbfa7bf2 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -38,9 +38,34 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe private var connectController: ConnectViewController? private weak var settingsNavController: SettingsNavigationController? private var lastLoginAction: LoginAction? - private var accountDataThrottling = AccountDataThrottling() + private lazy var accountDataThrottling = AccountDataThrottling(tunnelManager: tunnelManager) + private var outOfTimeTimer: Timer? + private var appDelegate: AppDelegate { + return UIApplication.shared.delegate as! AppDelegate + } + + private var storePaymentManager: StorePaymentManager { + return appDelegate.storePaymentManager + } + + private var relayCacheTracker: RelayCacheTracker { + return appDelegate.relayCacheTracker + } + + private var tunnelManager: TunnelManager { + return appDelegate.tunnelManager + } + + private var apiProxy: REST.APIProxy { + return appDelegate.apiProxy + } + + private var devicesProxy: REST.DevicesProxy { + return appDelegate.devicesProxy + } + deinit { clearOutOfTimeTimer() } @@ -79,7 +104,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe fatalError() } - RelayCacheTracker.shared.addObserver(self) + relayCacheTracker.addObserver(self) NotificationManager.shared.delegate = self accountDataThrottling.requestUpdate(condition: .always) @@ -113,15 +138,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe window?.makeKeyAndVisible() - TunnelManager.shared.addObserver(self) - if TunnelManager.shared.isConfigurationLoaded { + tunnelManager.addObserver(self) + if tunnelManager.isConfigurationLoaded { configureScene() } } - func sceneDidDisconnect(_ scene: UIScene) { - // no-op - } + func sceneDidDisconnect(_ scene: UIScene) {} func sceneDidBecomeActive(_ scene: UIScene) { if isSceneConfigured { @@ -200,21 +223,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController) -> Bool { - guard TunnelManager.shared.deviceState.isLoggedIn else { return false } + guard tunnelManager.deviceState.isLoggedIn else { return false } - switch TunnelManager.shared.tunnelStatus.state { + switch tunnelManager.tunnelStatus.state { case .connected, .connecting, .reconnecting, .waitingForConnectivity: - TunnelManager.shared.reconnectTunnel(selectNewRelay: true) + tunnelManager.reconnectTunnel(selectNewRelay: true) + case .disconnecting, .disconnected: - TunnelManager.shared.startTunnel() + tunnelManager.startTunnel() + case .pendingReconnect: break } + return true } private func setupPadUI() { - let tunnelManager = TunnelManager.shared let selectLocationController = makeSelectLocationController() let connectController = makeConnectViewController() @@ -241,7 +266,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe lazy var viewControllers: [UIViewController] = [self.makeLoginController()] - switch tunnelManager.deviceState { + switch self.tunnelManager.deviceState { case .loggedIn: let didDismissModalRoot = { self.handleExpiredAccount() @@ -313,7 +338,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe var viewControllers: [UIViewController] = [self.makeLoginController()] - switch TunnelManager.shared.deviceState { + switch self.tunnelManager.deviceState { case .loggedIn: let connectController = self.makeConnectViewController() self.connectController = connectController @@ -346,9 +371,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe { let navController = SettingsNavigationController( interactorFactory: SettingsInteractorFactory( - storePaymentManager: .shared, - tunnelManager: .shared, - apiProxy: REST.ProxyFactory.shared.createAPIProxy() + storePaymentManager: storePaymentManager, + tunnelManager: tunnelManager, + apiProxy: apiProxy ) ) navController.settingsDelegate = self @@ -370,8 +395,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe private func makeOutOfTimeViewController() -> OutOfTimeViewController { let viewController = OutOfTimeViewController( interactor: OutOfTimeInteractor( - storePaymentManager: .shared, - tunnelManager: .shared + storePaymentManager: storePaymentManager, + tunnelManager: tunnelManager ) ) viewController.delegate = self @@ -380,7 +405,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe private func makeConnectViewController() -> ConnectViewController { let connectController = ConnectViewController( - interactor: ConnectInteractor(tunnelManager: .shared) + interactor: ConnectInteractor(tunnelManager: tunnelManager) ) connectController.delegate = self @@ -391,11 +416,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe let selectLocationController = SelectLocationViewController() selectLocationController.delegate = self - if let cachedRelays = try? RelayCacheTracker.shared.getCachedRelays() { + if let cachedRelays = try? relayCacheTracker.getCachedRelays() { selectLocationController.setCachedRelays(cachedRelays) } - let relayConstraints = TunnelManager.shared.settings.relayConstraints + let relayConstraints = tunnelManager.settings.relayConstraints selectLocationController.setSelectedRelayLocation( relayConstraints.location.value, @@ -426,7 +451,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe private func makeRevokedDeviceController() -> RevokedDeviceViewController { let controller = RevokedDeviceViewController( - interactor: RevokedDeviceInteractor(tunnelManager: .shared) + interactor: RevokedDeviceInteractor(tunnelManager: tunnelManager) ) controller.delegate = self return controller @@ -439,7 +464,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe } private func handleExpiredAccount() { - guard case let .loggedIn(accountData, _) = TunnelManager.shared.deviceState, + guard case let .loggedIn(accountData, _) = tunnelManager.deviceState, accountData.expiry <= Date() else { return } switch UIDevice.current.userInterfaceIdiom { @@ -571,7 +596,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe ) { setEnableSettingsButton(isEnabled: false, from: controller) - TunnelManager.shared.setAccount(action: action.setAccountAction) { operationCompletion in + tunnelManager.setAccount(action: action.setAccountAction) { operationCompletion in switch operationCompletion { case .success: // RootContainer's settings button will be re-enabled in @@ -587,7 +612,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe self.lastLoginAction = action let deviceController = DeviceManagementViewController( - interactor: DeviceManagementInteractor(accountNumber: accountNumber) + interactor: DeviceManagementInteractor( + accountNumber: accountNumber, + devicesProxy: self.devicesProxy + ) ) deviceController.delegate = self @@ -621,7 +649,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe rootContainer.removeSettingsButtonFromPresentationContainer() setEnableSettingsButton(isEnabled: true, from: controller) - let relayConstraints = TunnelManager.shared.settings.relayConstraints + let relayConstraints = tunnelManager.settings.relayConstraints selectLocationViewController?.setSelectedRelayLocation( relayConstraints.location.value, animated: false, @@ -650,7 +678,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe private func setUpOutOfTimeTimer() { outOfTimeTimer?.invalidate() - guard case let .loggedIn(accountData, _) = TunnelManager.shared.deviceState, + guard case let .loggedIn(accountData, _) = tunnelManager.deviceState, accountData.expiry > Date() else { return } let timer = Timer( @@ -778,15 +806,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe private func selectLocationControllerDidSelectRelayLocation(_ relayLocation: RelayLocation) { let relayConstraints = RelayConstraints(location: .only(relayLocation)) - TunnelManager.shared.setRelayConstraints(relayConstraints) { - TunnelManager.shared.startTunnel() + tunnelManager.setRelayConstraints(relayConstraints) { + self.tunnelManager.startTunnel() } } // MARK: - RevokedDeviceViewControllerDelegate func revokedDeviceControllerDidRequestLogout(_ controller: RevokedDeviceViewController) { - TunnelManager.shared.unsetAccount { [weak self] in + tunnelManager.unsetAccount { [weak self] in self?.showLoginViewAfterLogout(dismissController: nil) } } diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift index fa8963f5b6..d9521bfda6 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift @@ -6,12 +6,12 @@ // Copyright © 2020 Mullvad VPN AB. All rights reserved. // -import Foundation import MullvadLogging import MullvadREST import MullvadTypes import Operations import StoreKit +import UIKit final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { private enum OperationCategory { @@ -19,13 +19,6 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { static let productsRequest = "StorePaymentManager.productsRequest" } - /// A shared instance of `AppStorePaymentManager` - static let shared = StorePaymentManager( - queue: SKPaymentQueue.default(), - apiProxy: REST.ProxyFactory.shared.createAPIProxy(), - accountsProxy: REST.ProxyFactory.shared.createAccountsProxy() - ) - private let logger = Logger(label: "StorePaymentManager") private let operationQueue: OperationQueue = { @@ -34,6 +27,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { return queue }() + private let application: UIApplication private let paymentQueue: SKPaymentQueue private let apiProxy: REST.APIProxy private let accountsProxy: REST.AccountsProxy @@ -69,7 +63,13 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { return SKPaymentQueue.canMakePayments() } - init(queue: SKPaymentQueue, apiProxy: REST.APIProxy, accountsProxy: REST.AccountsProxy) { + init( + application: UIApplication, + queue: SKPaymentQueue, + apiProxy: REST.APIProxy, + accountsProxy: REST.AccountsProxy + ) { + self.application = application paymentQueue = queue self.apiProxy = apiProxy self.accountsProxy = accountsProxy @@ -201,7 +201,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { } accountOperation.addObserver(BackgroundObserver( - application: .shared, + application: application, name: "Validate account number", cancelUponExpiration: false )) @@ -240,7 +240,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { operation.addObserver( BackgroundObserver( - application: .shared, + application: application, name: "Send AppStore receipt", cancelUponExpiration: true ) diff --git a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift index 7b0e49f70b..9b98cecfe0 100644 --- a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift +++ b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift @@ -11,11 +11,13 @@ import MullvadREST class TransportMonitor: TunnelObserver { private let tunnelManager: TunnelManager + private let transportRegistry: REST.TransportRegistry private let packetTunnelTransport: PacketTunnelTransport private let urlSessionTransport: REST.URLSessionTransport - init(tunnelManager: TunnelManager = .shared) { + init(tunnelManager: TunnelManager, transportRegistry: REST.TransportRegistry) { self.tunnelManager = tunnelManager + self.transportRegistry = transportRegistry packetTunnelTransport = PacketTunnelTransport(tunnelManager: tunnelManager) urlSessionTransport = REST.URLSessionTransport(urlSession: REST.makeURLSession()) @@ -49,7 +51,7 @@ class TransportMonitor: TunnelObserver { // MARK: - Private private func setTransports() { - REST.TransportRegistry.shared.setTransport( + transportRegistry.setTransport( stateUpdated( tunnelState: tunnelManager.tunnelStatus.state, deviceState: tunnelManager.deviceState diff --git a/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift index fd145e332a..42ed31c941 100644 --- a/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift @@ -36,15 +36,7 @@ class ReconnectTunnelOperation: ResultOperation<Void, Error> { } do { - var selectorResult: RelaySelectorResult? - - if selectNewRelay { - let cachedRelays = try RelayCacheTracker.shared.getCachedRelays() - selectorResult = try RelaySelector.evaluate( - relays: cachedRelays.relays, - constraints: interactor.settings.relayConstraints - ) - } + let selectorResult = selectNewRelay ? try interactor.selectRelay() : nil task = tunnel.reconnectTunnel( relaySelectorResult: selectorResult diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index d646103ce5..e2f9fa1c37 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -51,11 +51,7 @@ class StartTunnelOperation: ResultOperation<Void, Error> { case .disconnected, .pendingReconnect: do { - let cachedRelays = try RelayCacheTracker.shared.getCachedRelays() - let selectorResult = try RelaySelector.evaluate( - relays: cachedRelays.relays, - constraints: interactor.settings.relayConstraints - ) + let selectorResult = try interactor.selectRelay() makeTunnelProviderAndStartTunnel(selectorResult: selectorResult) { error in self.finish(completion: OperationCompletion(error: error)) diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift index f4fb16079b..eec47e1863 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift @@ -7,6 +7,8 @@ // import Foundation +import RelayCache +import RelaySelector protocol TunnelInteractor { // MARK: - Tunnel manipulation @@ -32,4 +34,5 @@ protocol TunnelInteractor { func startTunnel() func prepareForVPNConfigurationDeletion() + func selectRelay() throws -> RelaySelectorResult } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 727c8ecd8f..9bdc4ec8e9 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -12,6 +12,7 @@ import MullvadREST import MullvadTypes import NetworkExtension import Operations +import RelayCache import RelaySelector import StoreKit import TunnelProviderMessaging @@ -46,13 +47,10 @@ final class TunnelManager: StorePaymentObserver { } } - static let shared = TunnelManager( - accountsProxy: REST.ProxyFactory.shared.createAccountsProxy(), - devicesProxy: REST.ProxyFactory.shared.createDevicesProxy() - ) - // MARK: - Internal variables + private let application: UIApplication + private let relayCacheTracker: RelayCacheTracker private let accountsProxy: REST.AccountsProxy private let devicesProxy: REST.DevicesProxy @@ -87,7 +85,14 @@ final class TunnelManager: StorePaymentObserver { // MARK: - Initialization - private init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) { + init( + application: UIApplication, + relayCacheTracker: RelayCacheTracker, + accountsProxy: REST.AccountsProxy, + devicesProxy: REST.DevicesProxy + ) { + self.application = application + self.relayCacheTracker = relayCacheTracker self.accountsProxy = accountsProxy self.devicesProxy = devicesProxy self.operationQueue.name = "TunnelManager.operationQueue" @@ -106,8 +111,6 @@ final class TunnelManager: StorePaymentObserver { isRunningPeriodicPrivateKeyRotation = true updatePrivateKeyRotationTimer() - - nslock.unlock() } func stopPeriodicPrivateKeyRotation() { @@ -237,7 +240,7 @@ final class TunnelManager: StorePaymentObserver { groupOperation.addObserver( BackgroundObserver( - application: .shared, + application: application, name: "Load tunnel configuration", cancelUponExpiration: false ) @@ -290,7 +293,7 @@ final class TunnelManager: StorePaymentObserver { ) operation.addObserver(BackgroundObserver( - application: .shared, + application: application, name: "Start tunnel", cancelUponExpiration: true )) @@ -325,7 +328,7 @@ final class TunnelManager: StorePaymentObserver { } operation.addObserver(BackgroundObserver( - application: .shared, + application: application, name: "Stop tunnel", cancelUponExpiration: true )) @@ -353,7 +356,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: .shared, + application: application, name: "Reconnect tunnel", cancelUponExpiration: true ) @@ -385,7 +388,7 @@ final class TunnelManager: StorePaymentObserver { } operation.addObserver(BackgroundObserver( - application: .shared, + application: application, name: action.taskName, cancelUponExpiration: true )) @@ -423,7 +426,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: .shared, + application: application, name: "Update account data", cancelUponExpiration: true ) @@ -459,7 +462,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: .shared, + application: application, name: "Update device data", cancelUponExpiration: true ) @@ -514,7 +517,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: .shared, + application: application, name: "Rotate private key", cancelUponExpiration: true ) @@ -799,6 +802,15 @@ final class TunnelManager: StorePaymentObserver { // MARK: - Private methods + fileprivate func selectRelay() throws -> RelaySelectorResult { + let cachedRelays = try relayCacheTracker.getCachedRelays() + + return try RelaySelector.evaluate( + relays: cachedRelays.relays, + constraints: settings.relayConstraints + ) + } + fileprivate func prepareForVPNConfigurationDeletion() { nslock.lock() defer { nslock.unlock() } @@ -934,7 +946,7 @@ final class TunnelManager: StorePaymentObserver { } operation.addObserver(BackgroundObserver( - application: .shared, + application: application, name: taskName, cancelUponExpiration: false )) @@ -970,7 +982,7 @@ final class TunnelManager: StorePaymentObserver { } operation.addObserver(BackgroundObserver( - application: .shared, + application: application, name: taskName, cancelUponExpiration: false )) @@ -1068,4 +1080,8 @@ private struct TunnelInteractorProxy: TunnelInteractor { func prepareForVPNConfigurationDeletion() { tunnelManager.prepareForVPNConfigurationDeletion() } + + func selectRelay() throws -> RelaySelectorResult { + return try tunnelManager.selectRelay() + } } diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index b222e81720..d91f6b1193 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -73,12 +73,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { private var tunnelMonitor: TunnelMonitor! /// Account data request proxy - private lazy var accountProxy = REST.ProxyFactory.shared.createAccountsProxy() + private let accountsProxy: REST.AccountsProxy /// Device data request proxy - private lazy var deviceProxy = REST.ProxyFactory.shared.createDevicesProxy() + private let devicesProxy: REST.DevicesProxy - private lazy var checkDeviceStateTask: Cancellable? = nil + /// Last device check task. + private var checkDeviceStateTask: Cancellable? /// Internal operation queue. private let operationQueue = AsyncOperationQueue() @@ -108,11 +109,21 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger = Logger(label: "PacketTunnelProvider") tunnelLogger = Logger(label: "WireGuard") - super.init() - - REST.TransportRegistry.shared.setTransport( - REST.URLSessionTransport(urlSession: urlSession) + let addressCache = REST.AddressCache( + securityGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier, + isReadOnly: true + )! + let transportRegistry = REST.TransportRegistry( + transport: REST.URLSessionTransport(urlSession: urlSession) + ) + let proxyFactory = REST.ProxyFactory.makeProxyFactory( + transportRegistry: transportRegistry, + addressCache: addressCache ) + accountsProxy = proxyFactory.createAccountsProxy() + devicesProxy = proxyFactory.createDevicesProxy() + + super.init() adapter = WireGuardAdapter( with: self, @@ -172,7 +183,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } catch { providerLogger.error( error: error, - message: "Failed to start the tunnel." + message: "Failed to read tunnel configuration when starting the tunnel." ) completionHandler(error) @@ -593,7 +604,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { ) operation.setExecutionBlock { operation in - let task = self.accountProxy.getAccountData( + let task = self.accountsProxy.getAccountData( accountNumber: accountNumber, retryStrategy: .noRetry ) { completion in @@ -614,7 +625,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { let operation = ResultBlockOperation<REST.Device, REST.Error>(dispatchQueue: dispatchQueue) operation.setExecutionBlock { operation in - let task = self.deviceProxy.getDevice( + let task = self.devicesProxy.getDevice( accountNumber: accountNumber, identifier: identifier, retryStrategy: .noRetry |
