From 15365519223b1bd27285a59a22304c4e5d367d00 Mon Sep 17 00:00:00 2001 From: Davide Date: Mon, 18 Nov 2024 17:49:47 +0100 Subject: [PATCH] Prepare WireGuard for provider selector (#890) - Omit configuration on creation to show provider selector - Take out ConfigurationView like OpenVPN --- .../Library/Sources/AppUIMain/AppUIMain.swift | 3 +- .../WireGuardModule+Extensions.swift | 11 ++ .../Modules/WireGuardView+Configuration.swift | 114 +++++++++++++ .../Views/Modules/WireGuardView.swift | 150 +++++++++--------- .../CommonLibrary/Domain/ModuleType+New.swift | 6 +- .../UILibrary/L10n/SwiftGen+Strings.swift | 2 + .../Resources/en.lproj/Localizable.strings | 1 + 7 files changed, 203 insertions(+), 84 deletions(-) create mode 100644 Passepartout/Library/Sources/AppUIMain/Views/Modules/WireGuardView+Configuration.swift diff --git a/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift b/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift index d317a5b3..2f27feba 100644 --- a/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift +++ b/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift @@ -40,7 +40,8 @@ public final class AppUIMain: UILibraryConfiguring { private extension AppUIMain { func assertMissingImplementations(with registry: Registry) { let providerModuleTypes: Set = [ - .openVPN + .openVPN, + .wireGuard ] ModuleType.allCases.forEach { moduleType in do { diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift b/Passepartout/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift index 4eda2474..712710b6 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +import CommonUtils import PassepartoutKit import SwiftUI @@ -31,3 +32,13 @@ extension WireGuardModule.Builder: ModuleViewProviding { WireGuardView(editor: editor, module: self, impl: impl as? WireGuardModule.Implementation) } } + +extension WireGuardModule: ProviderEntityViewProviding { + func providerEntityView( + with provider: SerializedProvider, + errorHandler: ErrorHandler, + onSelect: @escaping (any ProviderEntity & Encodable) async throws -> Void + ) -> some View { + vpnProviderEntityView(with: provider, errorHandler: errorHandler, onSelect: onSelect) + } +} diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Modules/WireGuardView+Configuration.swift b/Passepartout/Library/Sources/AppUIMain/Views/Modules/WireGuardView+Configuration.swift new file mode 100644 index 00000000..f915d136 --- /dev/null +++ b/Passepartout/Library/Sources/AppUIMain/Views/Modules/WireGuardView+Configuration.swift @@ -0,0 +1,114 @@ +// +// WireGuardView+Configuration.swift +// Passepartout +// +// Created by Davide De Rosa on 10/28/24. +// Copyright (c) 2024 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import PassepartoutKit +import SwiftUI + +extension WireGuardView { + struct ConfigurationView: View { + let configuration: WireGuard.Configuration.Builder + + var body: some View { + moduleSection(for: interfaceRows, header: Strings.Modules.Wireguard.interface) + moduleSection(for: dnsRows, header: Strings.Unlocalized.dns) + ForEach(Array(zip(configuration.peers.indices, configuration.peers)), id: \.1.publicKey) { index, peer in + moduleSection(for: peersRows(for: peer), header: Strings.Modules.Wireguard.peer(index + 1)) + } + } + } +} + +private extension WireGuardView.ConfigurationView { + var interfaceRows: [ModuleRow]? { + var rows: [ModuleRow] = [] + rows.append(.longContent(caption: Strings.Global.privateKey, value: configuration.interface.privateKey)) + configuration.interface.addresses + .nilIfEmpty + .map { + rows.append(.textList( + caption: Strings.Global.addresses, + values: $0 + )) + } + configuration.interface.mtu.map { + rows.append(.text(caption: Strings.Unlocalized.mtu, value: $0.description)) + } + return rows.nilIfEmpty + } + + var dnsRows: [ModuleRow]? { + var rows: [ModuleRow] = [] + + configuration.interface.dns.servers + .nilIfEmpty + .map { + rows.append(.textList( + caption: Strings.Global.servers, + values: $0 + )) + } + + configuration.interface.dns.domainName.map { + rows.append(.text( + caption: Strings.Global.domain, + value: $0 + )) + } + + configuration.interface.dns.searchDomains? + .nilIfEmpty + .map { + rows.append(.textList( + caption: Strings.Entities.Dns.searchDomains, + values: $0 + )) + } + + return rows.nilIfEmpty + } + + func peersRows(for peer: WireGuard.RemoteInterface.Builder) -> [ModuleRow]? { + var rows: [ModuleRow] = [] + rows.append(.longContent(caption: Strings.Global.publicKey, value: peer.publicKey)) + peer.preSharedKey.map { + rows.append(.longContent(caption: Strings.Modules.Wireguard.presharedKey, value: $0)) + } + peer.endpoint.map { + rows.append(.copiableText(caption: Strings.Global.endpoint, value: $0)) + } + peer.allowedIPs + .nilIfEmpty + .map { + rows.append(.textList( + caption: Strings.Modules.Wireguard.allowedIps, + values: $0 + )) + } + peer.keepAlive.map { + rows.append(.text(caption: Strings.Global.keepAlive, value: TimeInterval($0).localizedDescription(style: .timeString))) + } + return rows.nilIfEmpty + } +} diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift b/Passepartout/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift index 0a3494a6..466d1dce 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift @@ -24,11 +24,15 @@ // import CommonLibrary +import CommonUtils import PassepartoutKit import SwiftUI struct WireGuardView: View, ModuleDraftEditing { + @Environment(\.navigationPath) + private var path + @ObservedObject var editor: ProfileEditor @@ -36,113 +40,103 @@ struct WireGuardView: View, ModuleDraftEditing { let impl: WireGuardModule.Implementation? + @State + private var paywallReason: PaywallReason? + + @State + private var errorHandler: ErrorHandler = .default() + var body: some View { contentView .moduleView(editor: editor, draft: draft.wrappedValue) + .modifier(PaywallModifier(reason: $paywallReason)) + .navigationDestination(for: Subroute.self, destination: destination) + .themeAnimation(on: providerId.wrappedValue, category: .modules) + .withErrorHandler(errorHandler) } } // MARK: - Content private extension WireGuardView { - var configuration: WireGuard.Configuration.Builder { - guard let impl else { - fatalError("Requires WireGuardModule implementation") - } - return draft.wrappedValue.configurationBuilder ?? .init(keyGenerator: impl.keyGenerator) - } @ViewBuilder var contentView: some View { - moduleSection(for: interfaceRows, header: Strings.Modules.Wireguard.interface) - moduleSection(for: dnsRows, header: Strings.Unlocalized.dns) - ForEach(Array(zip(configuration.peers.indices, configuration.peers)), id: \.1.publicKey) { index, peer in - moduleSection(for: peersRows(for: peer), header: Strings.Modules.Wireguard.peer(index + 1)) + if let configuration = draft.wrappedValue.configurationBuilder { + ConfigurationView(configuration: configuration) + } else { + EmptyView() + .modifier(providerModifier) } } -} -// MARK: - Subviews - -private extension WireGuardView { - var interfaceRows: [ModuleRow]? { - var rows: [ModuleRow] = [] - rows.append(.longContent(caption: Strings.Global.privateKey, value: configuration.interface.privateKey)) - configuration.interface.addresses - .nilIfEmpty - .map { - rows.append(.textList( - caption: Strings.Global.addresses, - values: $0 - )) + var providerModifier: some ViewModifier { + VPNProviderContentModifier( + providerId: providerId, + selectedEntity: providerEntity, + paywallReason: $paywallReason, + entityDestination: Subroute.providerServer, + providerRows: { + moduleGroup(for: providerKeyRows) } - configuration.interface.mtu.map { - rows.append(.text(caption: Strings.Unlocalized.mtu, value: $0.description)) - } - return rows.nilIfEmpty + ) } - var dnsRows: [ModuleRow]? { - var rows: [ModuleRow] = [] - - configuration.interface.dns.servers - .nilIfEmpty - .map { - rows.append(.textList( - caption: Strings.Global.servers, - values: $0 - )) - } - - configuration.interface.dns.domainName.map { - rows.append(.text( - caption: Strings.Global.domain, - value: $0 - )) - } - - configuration.interface.dns.searchDomains? - .nilIfEmpty - .map { - rows.append(.textList( - caption: Strings.Entities.Dns.searchDomains, - values: $0 - )) - } - - return rows.nilIfEmpty + var providerId: Binding { + editor.binding(forProviderOf: module.id) } - func peersRows(for peer: WireGuard.RemoteInterface.Builder) -> [ModuleRow]? { - var rows: [ModuleRow] = [] - rows.append(.longContent(caption: Strings.Global.publicKey, value: peer.publicKey)) - peer.preSharedKey.map { - rows.append(.longContent(caption: Strings.Modules.Wireguard.presharedKey, value: $0)) - } - peer.endpoint.map { - rows.append(.copiableText(caption: Strings.Global.endpoint, value: $0)) - } - peer.allowedIPs - .nilIfEmpty - .map { - rows.append(.textList( - caption: Strings.Modules.Wireguard.allowedIps, - values: $0 - )) - } - peer.keepAlive.map { - rows.append(.text(caption: Strings.Global.keepAlive, value: TimeInterval($0).localizedDescription(style: .timeString))) - } - return rows.nilIfEmpty + var providerEntity: Binding?> { + editor.binding(forProviderEntityOf: module.id) + } + + var providerKeyRows: [ModuleRow]? { + [.push(caption: Strings.Modules.Wireguard.providerKey, route: HashableRoute(Subroute.providerKey))] } } private extension WireGuardView { + func onSelectServer(server: VPNServer, preset: VPNPreset) { + providerEntity.wrappedValue = VPNEntity(server: server, preset: preset) + path.wrappedValue.removeLast() + } + func importConfiguration(from url: URL) { // TODO: #657, import draft from external URL } } +// MARK: - Destinations + +private extension WireGuardView { + enum Subroute: Hashable { + case providerServer + + case providerKey + } + + @ViewBuilder + func destination(for route: Subroute) -> some View { + switch route { + case .providerServer: + providerId.wrappedValue.map { + VPNProviderServerView( + moduleId: module.id, + providerId: $0, + configurationType: WireGuard.Configuration.self, + selectedEntity: providerEntity.wrappedValue, + filtersWithSelection: true, + onSelect: onSelectServer + ) + } + + case .providerKey: + // TODO: #339, WireGuard upload public key to provider + EmptyView() + } + } +} + // MARK: - Previews // swiftlint: disable force_try diff --git a/Passepartout/Library/Sources/CommonLibrary/Domain/ModuleType+New.swift b/Passepartout/Library/Sources/CommonLibrary/Domain/ModuleType+New.swift index 67142957..306d9f55 100644 --- a/Passepartout/Library/Sources/CommonLibrary/Domain/ModuleType+New.swift +++ b/Passepartout/Library/Sources/CommonLibrary/Domain/ModuleType+New.swift @@ -33,11 +33,7 @@ extension ModuleType { return OpenVPNModule.Builder() case .wireGuard: - 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)) + return WireGuardModule.Builder() case .dns: return DNSModule.Builder() diff --git a/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift b/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift index 9b5ccf8c..d5cbab41 100644 --- a/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift +++ b/Passepartout/Library/Sources/UILibrary/L10n/SwiftGen+Strings.swift @@ -510,6 +510,8 @@ public enum Strings { } /// Pre-shared key public static let presharedKey = Strings.tr("Localizable", "modules.wireguard.preshared_key", fallback: "Pre-shared key") + /// Private key + public static let providerKey = Strings.tr("Localizable", "modules.wireguard.provider_key", fallback: "Private key") } } public enum Paywall { diff --git a/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings b/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings index 97f3b6f5..408a76e8 100644 --- a/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Library/Sources/UILibrary/Resources/en.lproj/Localizable.strings @@ -233,6 +233,7 @@ "modules.openvpn.credentials.otp_method.approach.append" = "The OTP will be appended to the password."; "modules.openvpn.credentials.otp_method.approach.encode" = "The OTP will be encoded in Base64 with the password."; +"modules.wireguard.provider_key" = "Private key"; "modules.wireguard.interface" = "Interface"; "modules.wireguard.peer" = "Peer #%d"; "modules.wireguard.preshared_key" = "Pre-shared key";