Add logic to migrate v2 profiles (#854)
Will add UI separately. Fixes #642
This commit is contained in:
parent
83d77fafbb
commit
e514ade036
|
@ -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
|
||||
|
|
|
@ -20,3 +20,4 @@ default.profraw
|
|||
.env.secret*
|
||||
*.storekit
|
||||
tmp
|
||||
*.sqlite-*
|
||||
|
|
12
Gemfile.lock
12
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)
|
||||
|
|
|
@ -36,5 +36,5 @@ final class AppDelegate: NSObject {
|
|||
func configure(with uiConfiguring: UILibraryConfiguring) {
|
||||
UILibrary(uiConfiguring)
|
||||
.configure(with: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
|
||||
"state" : {
|
||||
"revision" : "caf31aff2e2641356de0d01f3c2c2d0d635d6a2b"
|
||||
"revision" : "3a4c78af67dfe181acc657a5539ee3d62d1c9361"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -32,6 +32,13 @@
|
|||
"identifier" : "CommonLibraryTests",
|
||||
"name" : "CommonLibraryTests"
|
||||
}
|
||||
},
|
||||
{
|
||||
"target" : {
|
||||
"containerPath" : "container:Passepartout\/Library",
|
||||
"identifier" : "LegacyV2Tests",
|
||||
"name" : "LegacyV2Tests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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"]
|
||||
|
|
Loading…
Reference in New Issue