diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c0596e2..35446d05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,5 +25,6 @@ jobs: access_token: ${{ secrets.ACCESS_TOKEN }} - name: Run tests run: | - cd Passepartout/Library - swift test + #cd Passepartout/Library + #swift test + bundle exec fastlane test diff --git a/.gitignore b/.gitignore index b72852fe..e6249374 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ default.profraw .env.secret* *.storekit tmp +*.sqlite-* diff --git a/Gemfile.lock b/Gemfile.lock index f3394373..06460bdf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,8 +58,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.1001.0) - aws-sdk-core (3.211.0) + aws-partitions (1.1004.0) + aws-sdk-core (3.212.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -67,7 +67,7 @@ GEM aws-sdk-kms (1.95.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.169.0) + aws-sdk-s3 (1.170.1) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -121,7 +121,7 @@ GEM fastlane-plugin-translate_gpt (0.1.8.2) loco_strings (~> 0.1.4.1) ruby-openai (~> 3.7) - fastlane-plugin-versioning (0.6.0) + fastlane-plugin-versioning (0.7.0) fastlane-sirp (1.0.0) sysrandom (~> 1.0) gh_inspector (1.1.3) @@ -170,7 +170,7 @@ GEM multi_xml (>= 0.5.2) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.5) + json (2.8.1) jwt (2.9.3) base64 loco_strings (0.1.4.1) @@ -190,7 +190,7 @@ GEM racc (~> 1.4) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) - optparse (0.5.0) + optparse (0.6.0) os (1.1.4) plist (3.7.1) public_suffix (6.0.1) diff --git a/Passepartout/App/AppDelegate.swift b/Passepartout/App/AppDelegate.swift index 9e6c0e53..0c33e2df 100644 --- a/Passepartout/App/AppDelegate.swift +++ b/Passepartout/App/AppDelegate.swift @@ -36,5 +36,5 @@ final class AppDelegate: NSObject { func configure(with uiConfiguring: UILibraryConfiguring) { UILibrary(uiConfiguring) .configure(with: context) - } + } } diff --git a/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/Library-Package.xcscheme b/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/Library-Package.xcscheme index 082343e1..d3e6193f 100644 --- a/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/Library-Package.xcscheme +++ b/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/Library-Package.xcscheme @@ -91,6 +91,20 @@ ReferencedContainer = "container:"> + + + + + + + + . +// + +import Foundation + +public struct MigratableProfile: Sendable { + public let id: UUID + + public let name: String + + public let lastUpdate: Date? + + public init(id: UUID, name: String, lastUpdate: Date?) { + self.id = id + self.name = name + self.lastUpdate = lastUpdate + } +} diff --git a/Passepartout/Library/Sources/CommonLibrary/Shared.swift b/Passepartout/Library/Sources/CommonLibrary/Shared.swift index 7104b8f2..cc591493 100644 --- a/Passepartout/Library/Sources/CommonLibrary/Shared.swift +++ b/Passepartout/Library/Sources/CommonLibrary/Shared.swift @@ -32,6 +32,8 @@ extension LoggerDestination { public enum App { public static let iap = LoggerDestination(category: "app.iap") + public static let migration = LoggerDestination(category: "app.migration") + public static let profiles = LoggerDestination(category: "app.profiles") } } diff --git a/Passepartout/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift b/Passepartout/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift index 335a24db..e746410d 100644 --- a/Passepartout/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift +++ b/Passepartout/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift @@ -35,13 +35,14 @@ public protocol CoreDataPersistentStoreLogger { } public final class CoreDataPersistentStore { - private let logger: CoreDataPersistentStoreLogger + private let logger: CoreDataPersistentStoreLogger? private let container: NSPersistentContainer public convenience init( - logger: CoreDataPersistentStoreLogger, + logger: CoreDataPersistentStoreLogger? = nil, containerName: String, + baseURL: URL? = nil, model: NSManagedObjectModel, cloudKitIdentifier: String?, author: String? @@ -49,10 +50,14 @@ public final class CoreDataPersistentStore { let container: NSPersistentContainer if let cloudKitIdentifier { container = NSPersistentCloudKitContainer(name: containerName, managedObjectModel: model) - logger.debug("Set up CloudKit container (\(cloudKitIdentifier)): \(containerName)") + logger?.debug("Set up CloudKit container (\(cloudKitIdentifier)): \(containerName)") } else { container = NSPersistentContainer(name: containerName, managedObjectModel: model) - logger.debug("Set up local container: \(containerName)") + logger?.debug("Set up local container: \(containerName)") + } + if let baseURL { + let url = baseURL.appending(component: "\(containerName).sqlite") + container.persistentStoreDescriptions = [.init(url: url)] } self.init( logger: logger, @@ -63,7 +68,7 @@ public final class CoreDataPersistentStore { } private init( - logger: CoreDataPersistentStoreLogger, + logger: CoreDataPersistentStoreLogger?, container: NSPersistentContainer, cloudKitIdentifier: String?, author: String? @@ -74,7 +79,7 @@ public final class CoreDataPersistentStore { guard let desc = container.persistentStoreDescriptions.first else { fatalError("Unable to read persistent store description") } - logger.debug("Container description: \(desc)") + logger?.debug("Container description: \(desc)") // optional container identifier for CloudKit, first in entitlements otherwise if let cloudKitIdentifier { @@ -98,7 +103,7 @@ public final class CoreDataPersistentStore { container.viewContext.automaticallyMergesChangesFromParent = true if let author { - logger.debug("Setting transaction author: \(author)") + logger?.debug("Setting transaction author: \(author)") container.viewContext.transactionAuthor = author } } @@ -141,7 +146,7 @@ extension CoreDataPersistentStore { try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: $0, options: nil) } } catch { - logger.warning("Unable to truncate persistent store: \(error)") + logger?.warning("Unable to truncate persistent store: \(error)") } } } diff --git a/Passepartout/Library/Sources/LegacyV2/CDProfileRepositoryV2.swift b/Passepartout/Library/Sources/LegacyV2/CDProfileRepositoryV2.swift index 1f867f12..99cf3019 100644 --- a/Passepartout/Library/Sources/LegacyV2/CDProfileRepositoryV2.swift +++ b/Passepartout/Library/Sources/LegacyV2/CDProfileRepositoryV2.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +import CommonLibrary import CoreData import Foundation import PassepartoutKit @@ -41,31 +42,78 @@ final class CDProfileRepositoryV2 { self.context = context } - // FIXME: #642, migrate profiles properly - func migratedProfiles() async throws -> [Profile] { + func migratableProfiles() async throws -> [MigratableProfile] { + try await fetchProfiles( + prefetch: { + $0.propertiesToFetch = ["uuid", "name", "lastUpdate"] + }, + map: { + $0.compactMap { + guard $0.value.encryptedJSON ?? $0.value.json != nil else { + pp_log(.App.migration, .error, "Unable to migrate profile \($0.key): missing JSON") + return nil + } + return MigratableProfile( + id: $0.key, + name: $0.value.name ?? $0.key.uuidString, + lastUpdate: $0.value.lastUpdate + ) + } + } + ) + } + + func profiles() async throws -> [ProfileV2] { + let decoder = JSONDecoder() + return try await fetchProfiles( + map: { + $0.compactMap { + guard let json = $0.value.encryptedJSON ?? $0.value.json else { + pp_log(.App.migration, .error, "Unable to migrate profile \($0.key): missing JSON") + return nil + } + do { + return try decoder.decode(ProfileV2.self, from: json) + } catch { + pp_log(.App.migration, .error, "Unable to migrate profile \($0.key): \(error)") + return nil + } + } + } + ) + } +} + +private extension CDProfileRepositoryV2 { + func fetchProfiles( + prefetch: ((NSFetchRequest) -> Void)? = nil, + map: @escaping ([UUID: CDProfile]) -> [T] + ) async throws -> [T] { try await context.perform { [weak self] in guard let self else { return [] } - do { - let request = CDProfile.fetchRequest() - let existing = try context.fetch(request) -// existing.forEach { -// guard let json = $0.encryptedJSON, -// let string = String(data: json, encoding: .utf8) else { -// return -// } -// print(">>> \(string)") -// } - return existing.compactMap { - guard let name = $0.name else { - return nil - } - return try? Profile.Builder(name: name).tryBuild() + + let request = CDProfile.fetchRequest() + request.sortDescriptors = [ + .init(key: "lastUpdate", ascending: false) + ] + prefetch?(request) + let existing = try context.fetch(request) + + var deduped: [UUID: CDProfile] = [:] + existing.forEach { + guard let uuid = $0.uuid else { + return } - } catch { - throw error + guard !deduped.keys.contains(uuid) else { + pp_log(.App.migration, .info, "Skip older duplicate of profile \(uuid)") + return + } + deduped[uuid] = $0 } + + return map(deduped) } } } diff --git a/Passepartout/Library/Sources/LegacyV2/CDProfile.swift b/Passepartout/Library/Sources/LegacyV2/Domain/CDProfile.swift similarity index 100% rename from Passepartout/Library/Sources/LegacyV2/CDProfile.swift rename to Passepartout/Library/Sources/LegacyV2/Domain/CDProfile.swift diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/MapperV2.swift b/Passepartout/Library/Sources/LegacyV2/Domain/MapperV2.swift new file mode 100644 index 00000000..adb85d2a --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/MapperV2.swift @@ -0,0 +1,186 @@ +// +// MapperV2.swift +// Passepartout +// +// Created by Davide De Rosa on 11/12/24. +// Copyright (c) 2024 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 PassepartoutKit + +struct MapperV2 { + func toProfileV3(_ v2: ProfileV2) throws -> Profile { + var builder = Profile.Builder(id: v2.id) + var modules: [Module] = [] + + builder.name = v2.header.name + builder.attributes.lastUpdate = v2.header.lastUpdate + + modules.append(toOnDemandModule(v2.onDemand)) + + if let provider = v2.provider { + if let module = try toProviderModule(provider) { + let providerId = ProviderID(rawValue: provider.name) + modules.append(module) + builder.setProviderId(providerId, forModuleWithId: module.id) + } + } else if let ovpn = v2.host?.ovpnSettings { + modules.append(try toOpenVPNModule(ovpn)) + } else if let wg = v2.host?.wgSettings { + modules.append(try toWireGuardModule(wg)) + } + + try toNetworkModules(v2.networkSettings).forEach { + modules.append($0) + } + + builder.modules = modules + builder.activeModulesIds = Set(modules.map(\.id)) + return try builder.tryBuild() + } +} + +extension MapperV2 { + func toOnDemandModule(_ v2: ProfileV2.OnDemand) -> OnDemandModule { + var builder = OnDemandModule.Builder() + builder.isEnabled = v2.isEnabled + switch v2.policy { + case .any: + builder.policy = .any + case .excluding: + builder.policy = .excluding + case .including: + builder.policy = .including + } + builder.withSSIDs = v2.withSSIDs + builder.withOtherNetworks = Set(v2.withOtherNetworks.map { + switch $0 { + case .ethernet: + return .ethernet + case .mobile: + return .mobile + } + }) + return builder.tryBuild() + } +} + +extension MapperV2 { + func toOpenVPNModule(_ v2: ProfileV2.OpenVPNSettings) throws -> OpenVPNModule { + var builder = OpenVPNModule.Builder() + builder.configurationBuilder = v2.configuration.builder() + builder.credentials = v2.account.map(toOpenVPNCredentials) + return try builder.tryBuild() + } + + func toOpenVPNCredentials(_ v2: ProfileV2.Account) -> OpenVPN.Credentials { + OpenVPN.Credentials.Builder(username: v2.username, password: v2.password) + .build() + } + + func toWireGuardModule(_ v2: ProfileV2.WireGuardSettings) throws -> WireGuardModule { + var builder = WireGuardModule.Builder() + builder.configurationBuilder = v2.configuration.configuration.builder() + return try builder.tryBuild() + } +} + +extension MapperV2 { + func toProviderModule(_ v2: ProfileV2.Provider) throws -> OpenVPNModule? { + assert(v2.vpnSettings.count == 1) + guard let entry = v2.vpnSettings.first else { + return nil + } + assert(entry.key == .openVPN) + let settings = entry.value + + var builder = OpenVPNModule.Builder() + builder.credentials = settings.account.map(toOpenVPNCredentials) + return try builder.tryBuild() + } +} + +extension MapperV2 { + func toNetworkModules(_ v2: ProfileV2.NetworkSettings) throws -> [Module] { + var modules: [Module] = [] + if v2.dns.choice == .manual { + modules.append(try toDNSModule(v2.dns)) + } + if v2.proxy.choice == .manual { + modules.append(try toHTTPProxyModule(v2.proxy)) + } + if v2.gateway.choice == .manual || v2.mtu.choice == .manual { + modules.append(try toIPModule(v2.gateway, v2MTU: v2.mtu)) + } + return modules + } + + func toDNSModule(_ v2: Network.DNSSettings) throws -> DNSModule { + var builder = DNSModule.Builder() + builder.protocolType = v2.dnsProtocol ?? .cleartext + builder.servers = v2.dnsServers ?? [] + builder.domainName = v2.dnsDomain + builder.searchDomains = v2.dnsSearchDomains + builder.dohURL = v2.dnsHTTPSURL?.absoluteString ?? "" + builder.dotHostname = v2.dnsTLSServerName ?? "" + return try builder.tryBuild() + } + + func toHTTPProxyModule(_ v2: Network.ProxySettings) throws -> HTTPProxyModule { + var builder = HTTPProxyModule.Builder() + builder.address = v2.proxyAddress ?? "" + builder.port = v2.proxyPort ?? 0 + builder.secureAddress = v2.proxyAddress ?? "" + builder.securePort = v2.proxyPort ?? 0 + builder.pacURLString = v2.proxyAutoConfigurationURL?.absoluteString ?? "" + builder.bypassDomains = v2.proxyBypassDomains ?? [] + return try builder.tryBuild() + } + + func toIPModule(_ v2Gateway: Network.GatewaySettings?, v2MTU: Network.MTUSettings?) throws -> IPModule { + var builder = IPModule.Builder() + + if let v2Gateway, v2Gateway.choice == .manual { + let defaultRoute = Route(defaultWithGateway: nil) + + if v2Gateway.isDefaultIPv4 { + builder.ipv4 = IPSettings(subnet: nil) + .including(routes: [defaultRoute]) + } else { + builder.ipv4 = IPSettings(subnet: nil) + .excluding(routes: [defaultRoute]) + } + + if v2Gateway.isDefaultIPv6 { + builder.ipv6 = IPSettings(subnet: nil) + .including(routes: [defaultRoute]) + } else { + builder.ipv6 = IPSettings(subnet: nil) + .excluding(routes: [defaultRoute]) + } + } + if let v2MTU, v2MTU.choice == .manual { + builder.mtu = v2MTU.mtuBytes + } + + return builder.tryBuild() + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Network.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Network.swift new file mode 100644 index 00000000..29c2609e --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Network.swift @@ -0,0 +1,159 @@ +// +// Network.swift +// Passepartout +// +// Created by Davide De Rosa on 2/15/22. +// Copyright (c) 2024 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 PassepartoutKit + +enum Network { +} + +extension Network { + enum Choice: String, Codable { + case automatic // OpenVPN pulls from server + + case manual + + static let defaultChoice: Choice = .automatic + } +} + +protocol NetworkChoiceRepresentable { + var choice: Network.Choice { get set } +} + +protocol GatewaySettingsProviding { + var isDefaultIPv4: Bool { get } + + var isDefaultIPv6: Bool { get } +} + +protocol DNSSettingsProviding { + var dnsProtocol: DNSProtocol? { get } + + var dnsServers: [String]? { get } + + var dnsDomain: String? { get } + + var dnsSearchDomains: [String]? { get } + + var dnsHTTPSURL: URL? { get } + + var dnsTLSServerName: String? { get } +} + +protocol ProxySettingsProviding { + var proxyServer: Endpoint? { get } + + var proxyBypassDomains: [String]? { get } + + var proxyAutoConfigurationURL: URL? { get } +} + +protocol MTUSettingsProviding { + var mtuBytes: Int { get } +} + +// + +extension Network { + struct GatewaySettings: Codable, Equatable, NetworkChoiceRepresentable, GatewaySettingsProviding { + var choice: Network.Choice + + var isDefaultIPv4 = true + + var isDefaultIPv6 = true + } +} + +extension Network { + struct DNSSettings: Codable, Equatable, NetworkChoiceRepresentable, DNSSettingsProviding { + enum ConfigurationType: String, Codable { + case plain + + case https + + case tls + + case disabled + } + + var choice: Network.Choice + + var configurationType: ConfigurationType = .plain + + var dnsProtocol: DNSProtocol? { + DNSProtocol(rawValue: configurationType.rawValue) + } + + var dnsServers: [String]? + + var dnsDomain: String? + + var dnsSearchDomains: [String]? + + var dnsHTTPSURL: URL? + + var dnsTLSServerName: String? + } +} + +extension Network { + struct ProxySettings: Codable, Equatable, NetworkChoiceRepresentable, ProxySettingsProviding { + enum ConfigurationType: String, Codable { + case manual + + case pac + + case disabled + } + + var choice: Network.Choice + + var configurationType: ConfigurationType = .manual + + var proxyAddress: String? + + var proxyPort: UInt16? + + var proxyBypassDomains: [String]? + + var proxyAutoConfigurationURL: URL? + + var proxyServer: Endpoint? { + guard let address = proxyAddress, let port = proxyPort, !address.isEmpty, port > 0 else { + return nil + } + return try? Endpoint(address, port) + } + } +} + +extension Network { + struct MTUSettings: Codable, Equatable, NetworkChoiceRepresentable, MTUSettingsProviding { + var choice: Network.Choice + + var mtuBytes = 0 + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Account.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Account.swift new file mode 100644 index 00000000..0d6238e6 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Account.swift @@ -0,0 +1,58 @@ +// +// Profile+Account.swift +// Passepartout +// +// Created by Davide De Rosa on 4/6/22. +// Copyright (c) 2024 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 + +extension ProfileV2 { + struct Account: Codable, Equatable { + enum AuthenticationMethod: String, Codable { + case persistent + + case interactive + + case totp + } + + var authenticationMethod: AuthenticationMethod? + + var username: String + + var password: String + + var isEmpty: Bool { + username.isEmpty && password.isEmpty + } + + init() { + username = "" + password = "" + } + + init(_ username: String, _ password: String) { + self.username = username + self.password = password + } + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Header.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Header.swift new file mode 100644 index 00000000..3d66c2d7 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Header.swift @@ -0,0 +1,70 @@ +// +// Profile+Header.swift +// Passepartout +// +// Created by Davide De Rosa on 2/17/22. +// Copyright (c) 2024 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 + +extension ProfileV2 { + struct Header: Codable, Identifiable, Hashable { + let uuid: UUID + + var name: String + + let providerName: ProviderName? + + let lastUpdate: Date? + + init( + uuid: UUID = UUID(), + name: String = "", + providerName: ProviderName? = nil, + lastUpdate: Date? = nil + ) { + self.uuid = uuid + self.name = name + self.providerName = providerName + self.lastUpdate = lastUpdate ?? Date() + } + + // MARK: Hashable + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.uuid == rhs.uuid && + lhs.name == rhs.name && + lhs.providerName == rhs.providerName + } + + func hash(into hasher: inout Hasher) { + hasher.combine(uuid) + hasher.combine(name) + hasher.combine(providerName) + } + + // MARK: Identifiable + + var id: UUID { + uuid + } + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Host.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Host.swift new file mode 100644 index 00000000..b6cd7513 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Host.swift @@ -0,0 +1,34 @@ +// +// Profile+Host.swift +// Passepartout +// +// Created by Davide De Rosa on 3/10/22. +// Copyright (c) 2024 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 + +extension ProfileV2 { + struct Host: Codable, Equatable { + var ovpnSettings: OpenVPNSettings? + + var wgSettings: WireGuardSettings? + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+NetworkSettings.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+NetworkSettings.swift new file mode 100644 index 00000000..833f0d66 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+NetworkSettings.swift @@ -0,0 +1,53 @@ +// +// Profile+NetworkSettings.swift +// Passepartout +// +// Created by Davide De Rosa on 2/15/22. +// Copyright (c) 2024 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 + +extension ProfileV2 { + struct NetworkSettings: Codable, Equatable { + var gateway: Network.GatewaySettings + + var dns: Network.DNSSettings + + var proxy: Network.ProxySettings + + var mtu: Network.MTUSettings + + var resolvesHostname = true + + var keepsAliveOnSleep = true + + init(choice: Network.Choice) { + gateway = Network.GatewaySettings(choice: choice) + dns = Network.DNSSettings(choice: choice) + proxy = Network.ProxySettings(choice: choice) + mtu = Network.MTUSettings(choice: choice) + } + + init() { + self.init(choice: .defaultChoice) + } + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OnDemand.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OnDemand.swift new file mode 100644 index 00000000..9cc609b4 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OnDemand.swift @@ -0,0 +1,55 @@ +// +// Profile+OnDemand.swift +// Passepartout +// +// Created by Davide De Rosa on 2/17/22. +// Copyright (c) 2024 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 + +extension ProfileV2 { + struct OnDemand: Codable, Equatable { + enum Policy: String, Codable { + case any + + case including + + case excluding // "trusted networks" + } + + enum OtherNetwork: String, Codable { + case mobile + + case ethernet + } + + var isEnabled = true + + var policy: Policy = .excluding + + var withSSIDs: [String: Bool] = [:] + + var withOtherNetworks: Set = [] + + init() { + } + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OpenVPNSettings.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OpenVPNSettings.swift new file mode 100644 index 00000000..910c9415 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OpenVPNSettings.swift @@ -0,0 +1,61 @@ +// +// Profile+OpenVPNSettings.swift +// Passepartout +// +// Created by Davide De Rosa on 2/16/22. +// Copyright (c) 2024 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 PassepartoutKit + +extension ProfileV2 { + struct OpenVPNSettings: Codable, Equatable, VPNProtocolProviding { + var vpnProtocol: VPNProtocolType { + .openVPN + } + + var configuration: OpenVPN.Configuration + + var account: Account? + + var customEndpoint: Endpoint? + + init(configuration: OpenVPN.Configuration) { + self.configuration = configuration + } + } + + init(_ id: UUID = UUID(), name: String, configuration: OpenVPN.Configuration) { + let header = Header( + uuid: id, + name: name, + providerName: nil + ) + self.init(header, configuration: configuration) + } + + init(_ header: Header, configuration: OpenVPN.Configuration) { + self.header = header + currentVPNProtocol = .openVPN + host = Host() + host?.ovpnSettings = OpenVPNSettings(configuration: configuration) + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Provider.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Provider.swift new file mode 100644 index 00000000..e4268bd1 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Provider.swift @@ -0,0 +1,58 @@ +// +// Profile+Provider.swift +// Passepartout +// +// Created by Davide De Rosa on 2/15/22. +// Copyright (c) 2024 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 PassepartoutKit + +typealias ProviderName = String + +extension ProfileV2 { + struct Provider: Codable, Equatable { + struct Settings: Codable, Equatable { + var account: Account? + + var serverId: String? + + var presetId: String? + + var favoriteLocationIds: Set? + + var customEndpoint: Endpoint? + + init() { + } + } + + let name: ProviderName + + var vpnSettings: [VPNProtocolType: Settings] = [:] + + var randomizesServer: Bool? + + init(_ name: ProviderName) { + self.name = name + } + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+WireGuardSettings.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+WireGuardSettings.swift new file mode 100644 index 00000000..04d7b0f0 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+WireGuardSettings.swift @@ -0,0 +1,79 @@ +// +// Profile+WireGuardSettings.swift +// Passepartout +// +// Created by Davide De Rosa on 2/17/22. +// Copyright (c) 2024 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 PassepartoutWireGuard +import PassepartoutWireGuardGo + +extension ProfileV2 { + struct WireGuardSettings: Codable, Equatable, VPNProtocolProviding { + struct WrappedConfiguration: Codable, Equatable { + let configuration: WireGuard.Configuration + + init(configuration: WireGuard.Configuration) { + self.configuration = configuration + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let wg = try container.decode(String.self) + configuration = try StandardWireGuardParser().configuration(from: wg) + } + + public func encode(to encoder: Encoder) throws { + let wg = try StandardWireGuardParser().string(from: configuration) + var container = encoder.singleValueContainer() + try container.encode(wg) + } + } + + var vpnProtocol: VPNProtocolType { + .wireGuard + } + + var configuration: WrappedConfiguration + + init(configuration: WireGuard.Configuration) { + self.configuration = WrappedConfiguration(configuration: configuration) + } + + } + + init(_ id: UUID = UUID(), name: String, configuration: WireGuard.Configuration) { + let header = Header( + uuid: id, + name: name, + providerName: nil + ) + self.init(header, configuration: configuration) + } + + init(_ header: Header, configuration: WireGuard.Configuration) { + self.header = header + currentVPNProtocol = .wireGuard + host = Host() + host?.wgSettings = WireGuardSettings(configuration: configuration) + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile.swift new file mode 100644 index 00000000..56ada650 --- /dev/null +++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile.swift @@ -0,0 +1,118 @@ +// +// Profile.swift +// Passepartout +// +// Created by Davide De Rosa on 2/11/22. +// Copyright (c) 2024 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 + +enum VPNProtocolType: String, RawRepresentable, Codable { + case openVPN = "ovpn" + + case wireGuard = "wg" +} + +protocol VPNProtocolProviding { + var vpnProtocol: VPNProtocolType { get } +} + +protocol ProfileSubtype { + var vpnProtocols: [VPNProtocolType] { get } +} + +struct ProfileV2: Identifiable, Codable, Equatable { + var header: Header + + var currentVPNProtocol: VPNProtocolType + + var networkSettings = NetworkSettings() + + var onDemand = OnDemand() + + var connectionExpirationDate: Date? + + var host: Host? + + var provider: Provider? + + init(_ header: Header) { + self.header = header + currentVPNProtocol = .openVPN + } + + init(_ id: UUID = UUID(), name: String) { + header = Header( + uuid: id, + name: name, + providerName: nil + ) + currentVPNProtocol = .openVPN + } + + init(_ id: UUID = UUID(), name: String, provider: Provider) { + let header = Header( + uuid: id, + name: name, + providerName: provider.name + ) + self.init(header, provider: provider) + } + + init(_ header: Header, provider: Provider) { + guard let firstVPNProtocol = provider.vpnSettings.keys.first else { + fatalError("No VPN protocols defined in provider") + } + self.header = header + currentVPNProtocol = firstVPNProtocol + self.provider = provider + } + + // MARK: Identifiable + + var id: UUID { + header.id + } +} + +extension ProfileV2 { + static let placeholder = ProfileV2( + UUID(uuidString: "00000000-0000-0000-0000-000000000000")!, + name: "" + ) + + static func isPlaceholder(_ id: UUID) -> Bool { + id == placeholder.id + } + + var isPlaceholder: Bool { + header.id == Self.placeholder.id + } +} + +extension ProfileV2 { + var isExpired: Bool { + guard let connectionExpirationDate else { + return false + } + return Date().distance(to: connectionExpirationDate) <= .zero + } +} diff --git a/Passepartout/Library/Sources/LegacyV2/LegacyV2.swift b/Passepartout/Library/Sources/LegacyV2/LegacyV2.swift index e0dce423..00d42423 100644 --- a/Passepartout/Library/Sources/LegacyV2/LegacyV2.swift +++ b/Passepartout/Library/Sources/LegacyV2/LegacyV2.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +import CommonLibrary import CommonUtils import Foundation import PassepartoutKit @@ -30,16 +31,18 @@ import PassepartoutKit public final class LegacyV2 { private let profilesRepository: CDProfileRepositoryV2 - private let cloudKitIdentifier: String + private let cloudKitIdentifier: String? public init( + coreDataLogger: CoreDataPersistentStoreLogger?, profilesContainerName: String, - cloudKitIdentifier: String, - coreDataLogger: CoreDataPersistentStoreLogger + baseURL: URL? = nil, + cloudKitIdentifier: String? ) { let store = CoreDataPersistentStore( logger: coreDataLogger, containerName: profilesContainerName, + baseURL: baseURL, model: CDProfileRepositoryV2.model, cloudKitIdentifier: cloudKitIdentifier, author: nil @@ -47,8 +50,42 @@ public final class LegacyV2 { profilesRepository = CDProfileRepositoryV2(context: store.context) self.cloudKitIdentifier = cloudKitIdentifier } +} - public func fetchProfiles() async throws -> [Profile] { - try await profilesRepository.migratedProfiles() +// MARK: - Mapping + +extension LegacyV2 { + public func fetchMigratableProfiles() async throws -> [MigratableProfile] { + try await profilesRepository.migratableProfiles() + } + + public func fetchProfiles(selection: Set) async throws -> (migrated: [Profile], failed: Set) { + let profilesV2 = try await profilesRepository.profiles() + + var migrated: [Profile] = [] + var failed: Set = [] + let mapper = MapperV2() + + profilesV2.forEach { + guard selection.contains($0.id) else { + return + } + do { + let mapped = try mapper.toProfileV3($0) + migrated.append(mapped) + } catch { + pp_log(.App.migration, .error, "Unable to migrate profile \($0.id): \(error)") + failed.insert($0.id) + } + } + return (migrated, failed) + } +} + +// MARK: - Legacy profiles + +extension LegacyV2 { + func fetchProfilesV2() async throws -> [ProfileV2] { + try await profilesRepository.profiles() } } diff --git a/Passepartout/Library/Sources/LegacyV2/Profiles.xcdatamodeld/Profiles.xcdatamodel/contents b/Passepartout/Library/Sources/LegacyV2/Profiles.xcdatamodeld/Profiles.xcdatamodel/contents index 347ee32b..d386b6f1 100644 --- a/Passepartout/Library/Sources/LegacyV2/Profiles.xcdatamodeld/Profiles.xcdatamodel/contents +++ b/Passepartout/Library/Sources/LegacyV2/Profiles.xcdatamodeld/Profiles.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Passepartout/Library/Tests/LegacyV2Tests/LegacyV2Tests.swift b/Passepartout/Library/Tests/LegacyV2Tests/LegacyV2Tests.swift new file mode 100644 index 00000000..91cede42 --- /dev/null +++ b/Passepartout/Library/Tests/LegacyV2Tests/LegacyV2Tests.swift @@ -0,0 +1,232 @@ +// +// LegacyV2Tests.swift +// Passepartout +// +// Created by Davide De Rosa on 11/12/24. +// Copyright (c) 2024 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 CommonUtils +import Foundation +@testable import LegacyV2 +import PassepartoutKit +import XCTest + +final class LegacyV2Tests: XCTestCase { + func test_givenStore_whenFetchV2_thenReturnsProfilesV2() async throws { + let sut = newStore() + + let profilesV2 = try await sut.fetchProfilesV2() + XCTAssertEqual(profilesV2.count, 6) + XCTAssertEqual(Set(profilesV2.map(\.header.name)), [ + "Hide.me", + "ProtonVPN", + "TorGuard", + "vps-ta-cert-cbc256-lzo", + "vps-wg", + "Windscribe" + ]) + } + + func test_givenStore_whenFetch_thenReturnsMigratableProfiles() async throws { + let sut = newStore() + + let migratable = try await sut.fetchMigratableProfiles() + let expectedIDs = [ + "069F76BD-1F6B-425C-AD83-62477A8B6558", + "239AD322-7440-4198-990A-D91379916FE2", + "38208B87-0545-4B11-A762-D04ED7CB904F", + "5D108793-7F62-4B4C-B194-0A7204C02E99", + "8A568345-85C4-44C1-A9C4-612E8B07ADC5", + "981E7CBD-7733-4CF3-9A51-2777614ED5D4" + ] + let expectedNames = [ + "Hide.me", + "ProtonVPN", + "TorGuard", + "vps-ta-cert-cbc256-lzo", + "vps-wg", + "Windscribe" + ] + + XCTAssertEqual(migratable.count, 6) + XCTAssertEqual(Set(migratable.map(\.id)), Set(expectedIDs.compactMap(UUID.init(uuidString:)))) + XCTAssertEqual(Set(migratable.map(\.name)), Set(expectedNames)) + } + + func test_givenStore_whenMigrateHideMe_thenIsExpected() async throws { + let sut = newStore() + + let id = try XCTUnwrap(UUID(uuidString: "8A568345-85C4-44C1-A9C4-612E8B07ADC5")) + let result = try await sut.fetchProfiles(selection: [id]) + let migrated = result.migrated + XCTAssertEqual(migrated.count, 1) + XCTAssertTrue(result.failed.isEmpty) + + let profile = try XCTUnwrap(migrated.first) + XCTAssertEqual(profile.id, id) + XCTAssertEqual(profile.name, "Hide.me") + XCTAssertEqual(profile.attributes.lastUpdate, Date(timeIntervalSinceReferenceDate: 673117681.24825)) + + XCTAssertEqual(profile.modules.count, 3) + + let onDemand = try XCTUnwrap(profile.firstModule(ofType: OnDemandModule.self)) + XCTAssertTrue(onDemand.isEnabled) + XCTAssertEqual(onDemand.policy, .excluding) + XCTAssertEqual(onDemand.withSSIDs, [ + "Safe Wi-Fi": true, + "Friend's House": false + ]) + XCTAssertTrue(onDemand.withOtherNetworks.isEmpty) + + let openVPN = try XCTUnwrap(profile.firstModule(ofType: OpenVPNModule.self)) + XCTAssertEqual(openVPN.credentials?.username, "foo") + XCTAssertEqual(openVPN.credentials?.password, "bar") + + let dns = try XCTUnwrap(profile.firstModule(ofType: DNSModule.self)) + let dohURL = try XCTUnwrap(URL(string: "https://1.1.1.1/dns-query")) + XCTAssertEqual(dns.protocolType, .https(url: dohURL)) + XCTAssertEqual(dns.servers, [ + Address(rawValue: "1.1.1.1"), + Address(rawValue: "1.0.0.1") + ]) + } + + func test_givenStore_whenMigrateProtonVPN_thenIsExpected() async throws { + let sut = newStore() + + let id = try XCTUnwrap(UUID(uuidString: "981E7CBD-7733-4CF3-9A51-2777614ED5D4")) + let result = try await sut.fetchProfiles(selection: [id]) + let migrated = result.migrated + XCTAssertEqual(migrated.count, 1) + XCTAssertTrue(result.failed.isEmpty) + + XCTAssertEqual(migrated.count, 1) + let profile = try XCTUnwrap(migrated.first) + XCTAssertEqual(profile.id, id) + XCTAssertEqual(profile.name, "ProtonVPN") + XCTAssertEqual(profile.attributes.lastUpdate, Date(timeIntervalSinceReferenceDate: 724509584.854822)) + + XCTAssertEqual(profile.modules.count, 2) + + let onDemand = try XCTUnwrap(profile.firstModule(ofType: OnDemandModule.self)) + XCTAssertTrue(onDemand.isEnabled) + XCTAssertEqual(onDemand.policy, .excluding) + XCTAssertTrue(onDemand.withSSIDs.isEmpty) + XCTAssertTrue(onDemand.withOtherNetworks.isEmpty) + + let openVPN = try XCTUnwrap(profile.firstModule(ofType: OpenVPNModule.self)) + XCTAssertEqual(openVPN.credentials?.username, "foo") + XCTAssertEqual(openVPN.credentials?.password, "bar") + } + + func test_givenStore_whenMigrateVPSOpenVPN_thenIsExpected() async throws { + let sut = newStore() + + let id = try XCTUnwrap(UUID(uuidString: "239AD322-7440-4198-990A-D91379916FE2")) + let result = try await sut.fetchProfiles(selection: [id]) + let migrated = result.migrated + XCTAssertEqual(migrated.count, 1) + XCTAssertTrue(result.failed.isEmpty) + + XCTAssertEqual(migrated.count, 1) + let profile = try XCTUnwrap(migrated.first) + XCTAssertEqual(profile.id, id) + XCTAssertEqual(profile.name, "vps-ta-cert-cbc256-lzo") + XCTAssertEqual(profile.attributes.lastUpdate, Date(timeIntervalSinceReferenceDate: 726164772.28976)) + + XCTAssertEqual(profile.modules.count, 2) + + let onDemand = try XCTUnwrap(profile.firstModule(ofType: OnDemandModule.self)) + XCTAssertTrue(onDemand.isEnabled) + XCTAssertEqual(onDemand.policy, .excluding) + XCTAssertTrue(onDemand.withSSIDs.isEmpty) + XCTAssertTrue(onDemand.withOtherNetworks.isEmpty) + + let openVPN = try XCTUnwrap(profile.firstModule(ofType: OpenVPNModule.self)) + XCTAssertNil(openVPN.credentials) + let cfg = try XCTUnwrap(openVPN.configuration) + XCTAssertEqual(cfg.remotes, [ + try .init("1.2.3.4", .init(.udp, 1198)) + ]) + XCTAssertEqual(cfg.authUserPass, false) + XCTAssertEqual(cfg.cipher, .aes256cbc) + XCTAssertEqual(cfg.digest, .sha256) + XCTAssertEqual(cfg.keepAliveInterval, 25.0) + XCTAssertEqual(cfg.checksEKU, true) + XCTAssertEqual(cfg.tlsWrap?.strategy, .auth) + } + + func test_givenStore_whenMigrateVPSWireGuard_thenIsExpected() async throws { + let sut = newStore() + + let id = try XCTUnwrap(UUID(uuidString: "069F76BD-1F6B-425C-AD83-62477A8B6558")) + let result = try await sut.fetchProfiles(selection: [id]) + let migrated = result.migrated + XCTAssertEqual(migrated.count, 1) + XCTAssertTrue(result.failed.isEmpty) + + XCTAssertEqual(migrated.count, 1) + let profile = try XCTUnwrap(migrated.first) + XCTAssertEqual(profile.id, id) + XCTAssertEqual(profile.name, "vps-wg") + XCTAssertEqual(profile.attributes.lastUpdate, Date(timeIntervalSinceReferenceDate: 727398252.46203)) + + XCTAssertEqual(profile.modules.count, 2) + + let onDemand = try XCTUnwrap(profile.firstModule(ofType: OnDemandModule.self)) + XCTAssertFalse(onDemand.isEnabled) + XCTAssertEqual(onDemand.policy, .including) + XCTAssertTrue(onDemand.withSSIDs.isEmpty) + XCTAssertTrue(onDemand.withOtherNetworks.isEmpty) + + let wireGuard = try XCTUnwrap(profile.firstModule(ofType: WireGuardModule.self)) + let cfg = try XCTUnwrap(wireGuard.configuration) + XCTAssertEqual(cfg.interface.privateKey.rawValue, "6L8Cv9zpG8RTDDwvZMhv6OR3kGdd+yATuKnMQWVLT1Q=") + XCTAssertEqual(cfg.interface.addresses, [ + try .init("4.5.6.7", 32) + ]) + XCTAssertEqual(cfg.interface.dns?.servers, [ + try XCTUnwrap(Address(rawValue: "1.1.1.1")) + ]) + XCTAssertNil(cfg.interface.mtu) + XCTAssertEqual(cfg.peers.count, 1) + let peer = try XCTUnwrap(cfg.peers.first) + XCTAssertEqual(peer.publicKey.rawValue, "JZc2trzk1WZTOUTjag1lcUZ2ePpFQYSpU2d0wqAw6mU=") + XCTAssertEqual(peer.endpoint?.rawValue, "8.8.8.8:55555") + XCTAssertEqual(peer.allowedIPs, [ + try .init("0.0.0.0", 0) + ]) + } +} + +private extension LegacyV2Tests { + func newStore() -> LegacyV2 { + guard let baseURL = Bundle.module.url(forResource: "Resources", withExtension: nil) else { + fatalError() + } + return LegacyV2( + coreDataLogger: nil, + profilesContainerName: "Profiles", + baseURL: baseURL, + cloudKitIdentifier: nil + ) + } +} diff --git a/Passepartout/Library/Tests/LegacyV2Tests/MapperV2Tests.swift b/Passepartout/Library/Tests/LegacyV2Tests/MapperV2Tests.swift new file mode 100644 index 00000000..c0ae1e51 --- /dev/null +++ b/Passepartout/Library/Tests/LegacyV2Tests/MapperV2Tests.swift @@ -0,0 +1,60 @@ +// +// MapperV2Tests.swift +// Passepartout +// +// Created by Davide De Rosa on 11/12/24. +// Copyright (c) 2024 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 +@testable import LegacyV2 +import PassepartoutKit +import XCTest + +final class MapperV2Tests: XCTestCase { + func test_givenMapper_whenDefaultGateway_thenIncludesDefaultRoute() throws { + let sut = MapperV2() + var settings = Network.GatewaySettings(choice: .manual) + var module: IPModule + + settings.isDefaultIPv4 = true + module = try sut.toIPModule(settings, v2MTU: nil) + XCTAssertTrue(module.ipv4?.includesDefaultRoute ?? false) + + settings.isDefaultIPv6 = true + module = try sut.toIPModule(settings, v2MTU: nil) + XCTAssertTrue(module.ipv6?.includesDefaultRoute ?? false) + } + + func test_givenMapper_whenNotDefaultGateway_thenExcludesDefaultRoute() throws { + let sut = MapperV2() + var settings = Network.GatewaySettings(choice: .manual) + var module: IPModule + let defaultRoute = Route(defaultWithGateway: nil) + + settings.isDefaultIPv4 = false + module = try sut.toIPModule(settings, v2MTU: nil) + XCTAssertTrue(module.ipv4?.excludedRoutes.contains(defaultRoute) ?? false) + + settings.isDefaultIPv6 = false + module = try sut.toIPModule(settings, v2MTU: nil) + XCTAssertTrue(module.ipv6?.excludedRoutes.contains(defaultRoute) ?? false) + } +} diff --git a/Passepartout/Library/Tests/LegacyV2Tests/Resources/Profiles.sqlite b/Passepartout/Library/Tests/LegacyV2Tests/Resources/Profiles.sqlite new file mode 100644 index 00000000..cf6bd637 Binary files /dev/null and b/Passepartout/Library/Tests/LegacyV2Tests/Resources/Profiles.sqlite differ diff --git a/Passepartout/Passepartout.xctestplan b/Passepartout/Passepartout.xctestplan index f59bfe2b..b4c3bdc6 100644 --- a/Passepartout/Passepartout.xctestplan +++ b/Passepartout/Passepartout.xctestplan @@ -32,6 +32,13 @@ "identifier" : "CommonLibraryTests", "name" : "CommonLibraryTests" } + }, + { + "target" : { + "containerPath" : "container:Passepartout\/Library", + "identifier" : "LegacyV2Tests", + "name" : "LegacyV2Tests" + } } ], "version" : 1 diff --git a/fastlane/.env b/fastlane/.env index 562f8922..24fd4515 100644 --- a/fastlane/.env +++ b/fastlane/.env @@ -2,6 +2,7 @@ LC_ALL="en_US.UTF-8" LANG="en_US.UTF-8" GYM_SCHEME="Passepartout" +SCAN_SCHEME="Passepartout" FL_VERSION_NUMBER_TARGET="Passepartout" FL_BUILD_NUMBER_PROJECT="Passepartout.xcodeproj" PILOT_BETA_APP_DESCRIPTION="Passepartout is your go-to app for VPN and privacy." diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 647ccd36..9e65c859 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -61,6 +61,15 @@ lane :bump do |options| ) end +desc "Run app tests" +lane :test do + scan( + clean: true, + xcargs: "CODE_SIGNING_ALLOWED=NO", + verbose: true + ) +end + desc "Push a new beta build to TestFlight" lane :beta do preface = ENV["TESTFLIGHT_PREFACE"]