// // ProviderManager.swift // Passepartout // // Created by Davide De Rosa on 3/13/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 Combine import PassepartoutCore import PassepartoutServices import PassepartoutUtils public final class ProviderManager: ObservableObject, RateLimited { private let appBuild: Int private let bundleServices: WebServices private let webServices: WebServices private let persistence: Persistence private let providerRepository: ProviderRepository private let infrastructureRepository: InfrastructureRepository private let serverRepository: ServerRepository public let didUpdateProviders = PassthroughSubject() public init(appBuild: Int, bundleServices: WebServices, webServices: WebServices, persistence: Persistence) { self.appBuild = appBuild self.bundleServices = bundleServices self.webServices = webServices self.persistence = persistence providerRepository = ProviderRepository(persistence.context) infrastructureRepository = InfrastructureRepository(persistence.context) serverRepository = ServerRepository(persistence.context) _ = allProviders() } // MARK: Queries public func allProviders() -> [ProviderMetadata] { providerRepository.allProviders() } public func provider(withName name: ProviderName) -> ProviderMetadata? { providerRepository.provider(withName: name) } public func isAvailable(_ name: ProviderName, vpnProtocol: VPNProtocolType) -> Bool { infrastructureRepository.lastInfrastructureUpdate(withName: name, vpnProtocol: vpnProtocol) != nil } public func defaultUsername(_ name: ProviderName, vpnProtocol: VPNProtocolType) -> String? { infrastructureRepository.defaultUsername(forProviderWithName: name, vpnProtocol: vpnProtocol) } public func lastUpdate(_ name: ProviderName, vpnProtocol: VPNProtocolType) -> Date? { infrastructureRepository.lastInfrastructureUpdate(withName: name, vpnProtocol: vpnProtocol) } public func categories(_ name: ProviderName, vpnProtocol: VPNProtocolType) -> [ProviderCategory] { serverRepository.categories(forProviderWithName: name, vpnProtocol: vpnProtocol) } public func servers(forLocation location: ProviderLocation) -> [ProviderServer] { serverRepository.servers(forLocation: location) } public func server(_ name: ProviderName, vpnProtocol: VPNProtocolType, apiId: String) -> ProviderServer? { serverRepository.server(forProviderWithName: name, vpnProtocol: vpnProtocol, apiId: apiId) } public func anyDefaultServer(_ name: ProviderName, vpnProtocol: VPNProtocolType) -> ProviderServer? { serverRepository.anyDefaultServer(forProviderWithName: name, vpnProtocol: vpnProtocol) } public func server(withId id: String) -> ProviderServer? { serverRepository.server(withId: id) } // MARK: Modification public func fetchProvidersIndexPublisher(priority: ProviderManagerFetchPriority) -> AnyPublisher { guard !isRateLimited(indexActionName) else { return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() } let publisher = priority.publisher(remote: { self.webServices.providersIndex() }, bundle: { self.bundleServices.providersIndex() }) return publisher .receive(on: DispatchQueue.main) .tryMap { index in self.saveLastAction(self.indexActionName) try self.providerRepository.mergeIndex(index) self.didUpdateProviders.send() }.eraseToAnyPublisher() } public func fetchProviderPublisher(withName providerName: ProviderName, vpnProtocol: VPNProtocolType, priority: ProviderManagerFetchPriority) -> AnyPublisher { guard !isRateLimited(providerName) else { return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() } let publisher = priority.publisher(remote: { let ifModifiedSince = self.infrastructureRepository.lastInfrastructureUpdate(withName: providerName, vpnProtocol: vpnProtocol) return self.webServices.providerNetwork( with: providerName.asWSProviderName, vpnProtocol: vpnProtocol.asWSVPNProtocol, ifModifiedSince: ifModifiedSince ) }, bundle: { self.bundleServices.providerNetwork( with: providerName.asWSProviderName, vpnProtocol: vpnProtocol.asWSVPNProtocol, ifModifiedSince: nil ) }) return publisher .receive(on: DispatchQueue.main) .flatMap { pub -> AnyPublisher in self.saveLastAction(providerName) // ignores empty responses (e.g. HTTP 304) guard let infrastructure = pub.value else { return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() } guard self.appBuild >= infrastructure.build else { pp_log.error("Infrastructure requires app build >= \(infrastructure.build) (app is \(self.appBuild))") return Fail(error: ProviderManagerError.outdatedBuild(self.appBuild, infrastructure.build)) .eraseToAnyPublisher() } do { try self.infrastructureRepository.saveInfrastructure( infrastructure, vpnProtocol: vpnProtocol, lastUpdate: pub.lastModified ?? Date() ) self.didUpdateProviders.send() } catch { pp_log.error("Unable to persist \(providerName) infrastructure (\(vpnProtocol)): \(error)") } return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() }.eraseToAnyPublisher() } public func reset() { persistence.truncate() didUpdateProviders.send() } // MARK: RateLimited private let indexActionName = "" public var lastActionDate: [String: Date] = [:] public var rateLimitMilliseconds: Int? } private enum ProviderManagerError: LocalizedError { case outdatedBuild(Int, Int) var errorDescription: String? { switch self { case .outdatedBuild(let current, let min): return "Build is outdated (found \(current), required \(min))" } } } private extension ProviderManagerFetchPriority { func publisher( remote: @escaping () -> AnyPublisher, bundle: @escaping () -> AnyPublisher ) -> AnyPublisher { switch self { case .bundle: return bundle() case .remote: return remote() case .remoteThenBundle: return remote() .catch { error -> AnyPublisher in pp_log.warning("Unable to fetch remotely: \(error)") pp_log.warning("Falling back to bundle") return bundle() }.eraseToAnyPublisher() } } } private extension ProviderName { var asWSProviderName: WSProviderName { self } } private extension VPNProtocolType { var asWSVPNProtocol: WSVPNProtocol { switch self { case .openVPN: return .openVPN case .wireGuard: return .wireGuard } } }