Add logic to migrate v2 profiles (#854)

Will add UI separately.

Fixes #642
This commit is contained in:
Davide 2024-11-12 16:42:19 +01:00 committed by GitHub
parent 83d77fafbb
commit e514ade036
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1454 additions and 44 deletions

View File

@ -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

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ default.profraw
.env.secret*
*.storekit
tmp
*.sqlite-*

View File

@ -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)

View File

@ -91,6 +91,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "LegacyV2"
BuildableName = "LegacyV2"
BlueprintName = "LegacyV2"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
@ -130,6 +144,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "LegacyV2Tests"
BuildableName = "LegacyV2Tests"
BlueprintName = "LegacyV2Tests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction

View File

@ -41,7 +41,7 @@
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
"revision" : "caf31aff2e2641356de0d01f3c2c2d0d635d6a2b"
"revision" : "3a4c78af67dfe181acc657a5539ee3d62d1c9361"
}
},
{

View File

@ -32,6 +32,10 @@ let package = Package(
name: "CommonLibrary",
targets: ["CommonLibrary"]
),
.library(
name: "LegacyV2",
targets: ["LegacyV2"]
),
.library(
name: "TunnelLibrary",
targets: [
@ -128,7 +132,8 @@ let package = Package(
.target(
name: "LegacyV2",
dependencies: [
"CommonUtils",
"CommonLibrary",
"PassepartoutImplementations",
.product(name: "PassepartoutKit", package: "passepartoutkit-source")
],
resources: [
@ -162,6 +167,13 @@ let package = Package(
name: "CommonLibraryTests",
dependencies: ["CommonLibrary"]
),
.testTarget(
name: "LegacyV2Tests",
dependencies: ["LegacyV2"],
resources: [
.copy("Resources")
]
),
.testTarget(
name: "UILibraryTests",
dependencies: ["UILibrary"]

View File

@ -0,0 +1,40 @@
//
// MigratableProfile.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 <http://www.gnu.org/licenses/>.
//
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
}
}

View File

@ -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")
}
}

View File

@ -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)")
}
}
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
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<T>(
prefetch: ((NSFetchRequest<CDProfile>) -> 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()
request.sortDescriptors = [
.init(key: "lastUpdate", ascending: false)
]
prefetch?(request)
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
var deduped: [UUID: CDProfile] = [:]
existing.forEach {
guard let uuid = $0.uuid else {
return
}
return try? Profile.Builder(name: name).tryBuild()
}
} 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)
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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()
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
import Foundation
extension ProfileV2 {
struct Host: Codable, Equatable {
var ovpnSettings: OpenVPNSettings?
var wgSettings: WireGuardSettings?
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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<OtherNetwork> = []
init() {
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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<String>?
var customEndpoint: Endpoint?
init() {
}
}
let name: ProviderName
var vpnSettings: [VPNProtocolType: Settings] = [:]
var randomizesServer: Bool?
init(_ name: ProviderName) {
self.name = name
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
}
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
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<UUID>) async throws -> (migrated: [Profile], failed: Set<UUID>) {
let profilesV2 = try await profilesRepository.profiles()
var migrated: [Profile] = []
var failed: Set<UUID> = []
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()
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="1.0">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23H124" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="CDProfile" representedClassName="CDProfile" syncable="YES">
<attribute name="encryptedJSON" optional="YES" attributeType="Binary" allowsCloudEncryption="YES"/>
<attribute name="json" optional="YES" attributeType="Binary"/>

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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
)
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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)
}
}

View File

@ -32,6 +32,13 @@
"identifier" : "CommonLibraryTests",
"name" : "CommonLibraryTests"
}
},
{
"target" : {
"containerPath" : "container:Passepartout\/Library",
"identifier" : "LegacyV2Tests",
"name" : "LegacyV2Tests"
}
}
],
"version" : 1

View File

@ -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."

View File

@ -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"]