Decouple library from PassepartoutKit implementations (#834)

Move the following dependencies:

- OpenVPN/OpenSSL
- WireGuard/Go

up the chain until the main App/Tunnel targets, so that UILibrary and
CommonLibrary can abstract from these unnecessary details. Instead, give
module views access to generic implementations via Registry.

Incidentally, this fixes an issue preventing TV previews from working
due to OpenSSL linkage.
This commit is contained in:
Davide 2024-11-08 12:37:09 +01:00 committed by GitHub
parent fed3a9b7d1
commit 7c27125dd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 145 additions and 52 deletions

View File

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

View File

@ -35,7 +35,7 @@ final class AppDelegate: NSObject {
func configure(with uiConfiguring: UILibraryConfiguring) {
UILibrary(uiConfiguring)
.configure()
.configure(with: context)
Task {
pp_log(.app, .notice, "Fetch providers index...")

View File

@ -15,7 +15,10 @@ let package = Package(
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "AppUI",
targets: ["AppUI"]
targets: [
"AppUI",
"PassepartoutImplementations"
]
),
.library(
name: "AppUIMain",
@ -31,7 +34,10 @@ let package = Package(
),
.library(
name: "TunnelLibrary",
targets: ["CommonLibrary"]
targets: [
"CommonLibrary",
"PassepartoutImplementations"
]
),
.library(
name: "UILibrary",
@ -40,7 +46,7 @@ let package = Package(
],
dependencies: [
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.0"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "e7bd9636ac31d6111b0bc7c171398e68eeb384b5"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "fe192115ca6f8e49447717dbe0a64347bd722aec"),
// .package(path: "../../../passepartoutkit-source"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),
@ -109,9 +115,7 @@ let package = Package(
name: "CommonLibrary",
dependencies: [
"CommonUtils",
.product(name: "PassepartoutKit", package: "passepartoutkit-source"),
.product(name: "PassepartoutOpenVPNOpenSSL", package: "passepartoutkit-source-openvpn-openssl"),
.product(name: "PassepartoutWireGuardGo", package: "passepartoutkit-source-wireguard-go")
.product(name: "PassepartoutKit", package: "passepartoutkit-source")
],
resources: [
.process("Resources")
@ -131,6 +135,13 @@ let package = Package(
.process("Profiles.xcdatamodeld")
]
),
.target(
name: "PassepartoutImplementations",
dependencies: [
.product(name: "PassepartoutOpenVPNOpenSSL", package: "passepartoutkit-source-openvpn-openssl"),
.product(name: "PassepartoutWireGuardGo", package: "passepartoutkit-source-wireguard-go")
]
),
.target(
name: "UILibrary",
dependencies: [

View File

@ -24,24 +24,25 @@
//
import Foundation
import PassepartoutKit
@_exported import UILibrary
public final class AppUIMain: UILibraryConfiguring {
public init() {
}
public func configure() {
assertMissingImplementations()
public func configure(with context: AppContext) {
assertMissingImplementations(with: context.registry)
}
}
private extension AppUIMain {
func assertMissingImplementations() {
func assertMissingImplementations(with registry: Registry) {
let providerModuleTypes: Set<ModuleType> = [
.openVPN
]
ModuleType.allCases.forEach { moduleType in
let builder = moduleType.newModule()
let builder = moduleType.newModule(with: registry)
guard builder is any ModuleViewProviding else {
fatalError("\(moduleType): is not ModuleViewProviding")
}

View File

@ -31,7 +31,7 @@ extension ModuleBuilder where Self: ModuleViewProviding {
@MainActor
func preview(title: String = "") -> some View {
NavigationStack {
moduleView(with: ProfileEditor(modules: [self]))
moduleView(with: ProfileEditor(modules: [self]), impl: nil)
.navigationTitle(title)
}
.withMockEnvironment()

View File

@ -28,19 +28,24 @@ import PassepartoutKit
import SwiftUI
final class DefaultModuleViewFactory: ModuleViewFactory {
private let registry: Registry
init(registry: Registry) {
self.registry = registry
}
@ViewBuilder
func view(with editor: ProfileEditor, moduleId: UUID) -> some View {
let result = editor.moduleViewProvider(withId: moduleId)
let result = editor.moduleViewProvider(withId: moduleId, registry: registry)
if let result {
AnyView(result.provider.moduleView(with: editor))
AnyView(result.provider.moduleView(with: editor, impl: result.impl))
.navigationTitle(result.title)
}
}
}
private extension ProfileEditor {
func moduleViewProvider(withId moduleId: UUID) -> (provider: any ModuleViewProviding, title: String)? {
func moduleViewProvider(withId moduleId: UUID, registry: Registry) -> ModuleViewProviderResult? {
guard let module = module(withId: moduleId) else {
// assertionFailure("No module with ID \(moduleId)")
return nil
@ -49,6 +54,18 @@ private extension ProfileEditor {
assertionFailure("\(type(of: module)) does not provide a default view")
return nil
}
return (provider, module.moduleType.localizedDescription)
return ModuleViewProviderResult(
title: module.moduleType.localizedDescription,
provider: provider,
impl: registry.implementation(for: module)
)
}
}
private struct ModuleViewProviderResult {
let title: String
let provider: any ModuleViewProviding
let impl: ModuleImplementation?
}

View File

@ -23,11 +23,12 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import PassepartoutKit
import SwiftUI
protocol ModuleViewProviding {
associatedtype Content: View
@MainActor
func moduleView(with editor: ProfileEditor) -> Content
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> Content
}

View File

@ -157,7 +157,8 @@ extension AppCoordinator {
ProfileCoordinator(
profileManager: profileManager,
profileEditor: profileEditor,
moduleViewFactory: DefaultModuleViewFactory(),
registry: registry,
moduleViewFactory: DefaultModuleViewFactory(registry: registry),
modally: true,
path: $profilePath,
onDismiss: {

View File

@ -27,7 +27,7 @@ import PassepartoutKit
import SwiftUI
extension DNSModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
DNSView(editor: editor, module: self)
}
}

View File

@ -27,7 +27,7 @@ import PassepartoutKit
import SwiftUI
extension HTTPProxyModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
HTTPProxyView(editor: editor, module: self)
}
}

View File

@ -27,7 +27,7 @@ import PassepartoutKit
import SwiftUI
extension IPModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
IPView(editor: editor, module: self)
}
}

View File

@ -27,7 +27,7 @@ import PassepartoutKit
import SwiftUI
extension OnDemandModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
OnDemandView(editor: editor, module: self)
}
}

View File

@ -28,8 +28,8 @@ import PassepartoutKit
import SwiftUI
extension OpenVPNModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
OpenVPNView(editor: editor, module: self)
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
OpenVPNView(editor: editor, module: self, impl: impl as? OpenVPNModule.Implementation)
}
}

View File

@ -27,7 +27,7 @@ import PassepartoutKit
import SwiftUI
extension WireGuardModule.Builder: ModuleViewProviding {
func moduleView(with editor: ProfileEditor) -> some View {
WireGuardView(editor: editor, module: self)
func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View {
WireGuardView(editor: editor, module: self, impl: impl as? WireGuardModule.Implementation)
}
}

View File

@ -25,7 +25,6 @@
import CommonLibrary
import CommonUtils
import CPassepartoutOpenVPNOpenSSL
import PassepartoutKit
import SwiftUI
@ -39,6 +38,8 @@ struct OpenVPNView: View, ModuleDraftEditing {
let module: OpenVPNModule.Builder
let impl: OpenVPNModule.Implementation?
private let isServerPushed: Bool
@State
@ -66,12 +67,14 @@ struct OpenVPNView: View, ModuleDraftEditing {
self.editor = editor
self.module = module
impl = nil
isServerPushed = true
}
init(editor: ProfileEditor, module: OpenVPNModule.Builder) {
init(editor: ProfileEditor, module: OpenVPNModule.Builder, impl: OpenVPNModule.Implementation?) {
self.editor = editor
self.module = module
self.impl = impl
isServerPushed = false
}
@ -179,8 +182,13 @@ private extension OpenVPNView {
}
importURL = url
let parsed = try StandardOpenVPNParser(decrypter: OSSLTLSBox())
.parsed(fromURL: url, passphrase: importPassphrase)
guard let impl else {
fatalError("Requires OpenVPNModule implementation")
}
guard let parser = impl.importer as? StandardOpenVPNParser else {
fatalError("OpenVPNModule importer should be StandardOpenVPNParser")
}
let parsed = try parser.parsed(fromURL: url, passphrase: importPassphrase)
draft.wrappedValue.configurationBuilder = parsed.configuration.builder()
} catch StandardOpenVPNParserError.encryptionPassphrase,

View File

@ -25,7 +25,6 @@
import CommonLibrary
import PassepartoutKit
import PassepartoutWireGuardGo
import SwiftUI
struct WireGuardView: View, ModuleDraftEditing {
@ -35,6 +34,8 @@ struct WireGuardView: View, ModuleDraftEditing {
let module: WireGuardModule.Builder
let impl: WireGuardModule.Implementation?
var body: some View {
contentView
.moduleView(editor: editor, draft: draft.wrappedValue)
@ -45,7 +46,10 @@ struct WireGuardView: View, ModuleDraftEditing {
private extension WireGuardView {
var configuration: WireGuard.Configuration.Builder {
draft.wrappedValue.configurationBuilder ?? .default
guard let impl else {
fatalError("Requires WireGuardModule implementation")
}
return draft.wrappedValue.configurationBuilder ?? .init(keyGenerator: impl.keyGenerator)
}
@ViewBuilder
@ -143,7 +147,7 @@ private extension WireGuardView {
// swiftlint: disable force_try
#Preview {
let gen = StandardWireGuardKeyGenerator()
let gen = MockGenerator()
var builder = WireGuard.Configuration.Builder(keyGenerator: gen)
builder.interface.addresses = ["1.1.1.1", "2.2.2.2"]
@ -166,3 +170,21 @@ private extension WireGuardView {
return module.preview()
}
// swiftlint: enable force_try
private final class MockGenerator: WireGuardKeyGenerator {
func newPrivateKey() -> String {
"private-key"
}
func privateKey(from string: String) throws -> String {
"private-key"
}
func publicKey(from string: String) throws -> String {
"public-key"
}
func publicKey(for privateKey: String) throws -> String {
"public-key"
}
}

View File

@ -62,7 +62,7 @@ private extension ModuleDetailView {
ModuleDetailView(
profileEditor: ProfileEditor(profile: .mock),
moduleId: Profile.mock.modules.first?.id,
moduleViewFactory: DefaultModuleViewFactory()
moduleViewFactory: DefaultModuleViewFactory(registry: Registry())
)
.withMockEnvironment()
}

View File

@ -47,6 +47,8 @@ struct ProfileCoordinator: View {
let profileEditor: ProfileEditor
let registry: Registry
let moduleViewFactory: any ModuleViewFactory
let modally: Bool
@ -125,7 +127,7 @@ private extension ProfileCoordinator {
return
}
let module = moduleType.newModule()
let module = moduleType.newModule(with: registry)
withAnimation(theme.animation(for: .modules)) {
profileEditor.saveModule(module, activating: true)
}
@ -152,7 +154,8 @@ private extension ProfileCoordinator {
ProfileCoordinator(
profileManager: .mock,
profileEditor: ProfileEditor(profile: .newMockProfile()),
moduleViewFactory: DefaultModuleViewFactory(),
registry: Registry(),
moduleViewFactory: DefaultModuleViewFactory(registry: Registry()),
modally: false,
path: .constant(NavigationPath()),
onDismiss: {}

View File

@ -123,7 +123,7 @@ private extension ProfileSplitView {
#Preview {
ProfileSplitView(
profileEditor: ProfileEditor(profile: .newMockProfile()),
moduleViewFactory: DefaultModuleViewFactory()
moduleViewFactory: DefaultModuleViewFactory(registry: Registry())
)
.withMockEnvironment()
}

View File

@ -30,6 +30,6 @@ public final class AppUITV: UILibraryConfiguring {
public init() {
}
public func configure() {
public func configure(with context: AppContext) {
}
}

View File

@ -25,7 +25,6 @@
import Foundation
import PassepartoutKit
import PassepartoutWireGuardGo
extension LoggerDestination {
public static let app = LoggerDestination(category: "app")
@ -37,12 +36,6 @@ extension LoggerDestination {
}
}
extension WireGuard.Configuration.Builder {
public static var `default`: Self {
.init(keyGenerator: StandardWireGuardKeyGenerator())
}
}
// TODO: #716, move to Environment
extension Constants {
public static let shared = Bundle.module.unsafeDecode(Constants.self, filename: "Constants")

View File

@ -0,0 +1,26 @@
//
// Dummy.swift
// Passepartout
//
// Created by Davide De Rosa on 11/8/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

View File

@ -27,13 +27,17 @@ import Foundation
import PassepartoutKit
extension ModuleType {
public func newModule() -> any ModuleBuilder {
public func newModule(with registry: Registry) -> any ModuleBuilder {
switch self {
case .openVPN:
return OpenVPNModule.Builder()
case .wireGuard:
return WireGuardModule.Builder(configurationBuilder: .default)
let impl = registry.implementation(for: WireGuardModule.moduleHandler.id)
guard let wireGuard = impl as? WireGuardModule.Implementation else {
fatalError("Missing WireGuardModule implementation from Registry?")
}
return WireGuardModule.Builder(configurationBuilder: .init(keyGenerator: wireGuard.keyGenerator))
case .dns:
return DNSModule.Builder()

View File

@ -25,7 +25,6 @@
import Foundation
import PassepartoutKit
import PassepartoutWireGuardGo
public struct ModuleType: RawRepresentable, Hashable {
public let rawValue: String

View File

@ -30,7 +30,7 @@ import PassepartoutKit
@MainActor
public protocol UILibraryConfiguring {
func configure()
func configure(with context: AppContext)
}
public final class UILibrary: UILibraryConfiguring {
@ -40,12 +40,12 @@ public final class UILibrary: UILibraryConfiguring {
self.uiConfiguring = uiConfiguring
}
public func configure() {
public func configure(with context: AppContext) {
PassepartoutConfiguration.shared.configureLogging(
to: BundleConfiguration.urlForAppLog,
parameters: Constants.shared.log,
logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key)
)
uiConfiguring?.configure()
uiConfiguring?.configure(with: context)
}
}

View File

@ -255,3 +255,9 @@ extension ProfileEditorTests {
await fulfillment(of: [exp])
}
}
private extension WireGuard.Configuration.Builder {
static var `default`: WireGuard.Configuration.Builder {
WireGuard.Configuration.Builder(privateKey: "")
}
}

View File

@ -55,6 +55,7 @@ extension Registry {
}
),
WireGuardModule.Implementation(
keyGenerator: StandardWireGuardKeyGenerator(),
importer: StandardWireGuardParser(),
connectionBlock: { parameters, module in
try GoWireGuardConnection(parameters: parameters, module: module)