diff --git a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index de0b3b05..72ba4a43 100644
--- a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -41,7 +41,7 @@
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
- "revision" : "efbf2e135b04e9c0f67b5d2b517c5e13a75c50f6"
+ "revision" : "d21f1b362dfe667c36483e18b2dbb494bba54660"
}
},
{
diff --git a/Passepartout/Library/Package.swift b/Passepartout/Library/Package.swift
index 144d9b9d..6985d19f 100644
--- a/Passepartout/Library/Package.swift
+++ b/Passepartout/Library/Package.swift
@@ -28,7 +28,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: "efbf2e135b04e9c0f67b5d2b517c5e13a75c50f6"),
+ .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "d21f1b362dfe667c36483e18b2dbb494bba54660"),
// .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"),
diff --git a/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift b/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift
index 6ca20e1f..857a07ef 100644
--- a/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift
+++ b/Passepartout/Library/Sources/AppUI/L10n/AppError+L10n.swift
@@ -65,19 +65,24 @@ extension PassepartoutError: LocalizedError {
return Strings.Errors.App.Passepartout.incompatibleModules
case .invalidFields:
- guard let fields = userInfo as? [String: String?] else {
- return nil
- }
- let fieldsDescription = fields
+ let fields = (userInfo as? [String: String?])
.map {
- "\($0)=\($1?.description ?? "")"
+ $0.map {
+ "\($0)=\($1?.description ?? "")"
+ }
+ .joined(separator: ",")
}
- .joined(separator: ",")
- return Strings.Errors.App.Passepartout.invalidFields(fieldsDescription)
+ return [Strings.Errors.App.Passepartout.invalidFields, fields]
+ .compactMap { $0 }
+ .joined(separator: " ")
case .parsing:
- return reason?.localizedDescription ?? Strings.Errors.App.Passepartout.parsing
+ let message = userInfo as? String ?? reason?.localizedDescription
+
+ return [Strings.Errors.App.Passepartout.parsing, message]
+ .compactMap { $0 }
+ .joined(separator: " ")
case .unhandled:
return reason?.localizedDescription
diff --git a/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift b/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift
index ed47c9c6..6c931a8b 100644
--- a/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift
+++ b/Passepartout/Library/Sources/AppUI/L10n/SwiftGen+Strings.swift
@@ -27,6 +27,16 @@ public enum Strings {
public static let title = Strings.tr("Localizable", "alerts.iap.restricted.title", fallback: "Restricted")
}
}
+ public enum Import {
+ public enum Passphrase {
+ /// Enter passphrase for '%@'.
+ public static func message(_ p1: Any) -> String {
+ return Strings.tr("Localizable", "alerts.import.passphrase.message", String(describing: p1), fallback: "Enter passphrase for '%@'.")
+ }
+ /// Decrypt
+ public static let ok = Strings.tr("Localizable", "alerts.import.passphrase.ok", fallback: "Decrypt")
+ }
+ }
}
public enum Entities {
public enum ConnectionStatus {
@@ -121,10 +131,8 @@ public enum Strings {
}
/// Some active modules are incompatible, try to only activate one of them.
public static let incompatibleModules = Strings.tr("Localizable", "errors.app.passepartout.incompatible_modules", fallback: "Some active modules are incompatible, try to only activate one of them.")
- /// Invalid fields (%@).
- public static func invalidFields(_ p1: Any) -> String {
- return Strings.tr("Localizable", "errors.app.passepartout.invalid_fields", String(describing: p1), fallback: "Invalid fields (%@).")
- }
+ /// Invalid fields.
+ public static let invalidFields = Strings.tr("Localizable", "errors.app.passepartout.invalid_fields", fallback: "Invalid fields.")
/// Unable to parse file.
public static let parsing = Strings.tr("Localizable", "errors.app.passepartout.parsing", fallback: "Unable to parse file.")
}
@@ -313,6 +321,8 @@ public enum Strings {
public enum Rows {
/// Shared on iCloud
public static let icloudSharing = Strings.tr("Localizable", "modules.general.rows.icloud_sharing", fallback: "Shared on iCloud")
+ /// Import from file...
+ public static let importFromFile = Strings.tr("Localizable", "modules.general.rows.import_from_file", fallback: "Import from file...")
public enum IcloudSharing {
/// Share on iCloud
public static let purchase = Strings.tr("Localizable", "modules.general.rows.icloud_sharing.purchase", fallback: "Share on iCloud")
@@ -598,18 +608,6 @@ public enum Strings {
}
}
public enum Profiles {
- public enum Alerts {
- public enum Import {
- public enum Passphrase {
- /// Enter passphrase for '%@'.
- public static func message(_ p1: Any) -> String {
- return Strings.tr("Localizable", "views.profiles.alerts.import.passphrase.message", String(describing: p1), fallback: "Enter passphrase for '%@'.")
- }
- /// Decrypt
- public static let ok = Strings.tr("Localizable", "views.profiles.alerts.import.passphrase.ok", fallback: "Decrypt")
- }
- }
- }
public enum Errors {
/// Unable to duplicate profile '%@'.
public static func duplicate(_ p1: Any) -> String {
diff --git a/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings b/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings
index 70bac877..f3879e1c 100644
--- a/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings
+++ b/Passepartout/Library/Sources/AppUI/Resources/en.lproj/Localizable.strings
@@ -124,8 +124,6 @@
"views.profiles.folders.no_profiles" = "No profiles";
"views.profiles.toolbar.new_profile" = "New profile";
"views.profiles.toolbar.import_profile" = "Import profile";
-"views.profiles.alerts.import.passphrase.message" = "Enter passphrase for '%@'.";
-"views.profiles.alerts.import.passphrase.ok" = "Decrypt";
"views.profiles.errors.tunnel" = "Unable to execute tunnel operation.";
"views.profiles.errors.duplicate" = "Unable to duplicate profile '%@'.";
"views.profiles.errors.import" = "Unable to import profiles.";
@@ -175,6 +173,7 @@
"modules.general.sections.storage.footer" = "Profiles are stored to iCloud encrypted.";
"modules.general.rows.icloud_sharing" = "Shared on iCloud";
+"modules.general.rows.import_from_file" = "Import from file...";
"modules.dns.servers.add" = "Add address";
"modules.dns.search_domains.add" = "Add domain";
@@ -248,6 +247,9 @@
// MARK: - Alerts
+"alerts.import.passphrase.message" = "Enter passphrase for '%@'.";
+"alerts.import.passphrase.ok" = "Decrypt";
+
"alerts.iap.restricted.title" = "Restricted";
"alerts.iap.restricted.message" = "The requested feature is unavailable in this build.";
@@ -262,7 +264,7 @@
"errors.app.default" = "Unable to complete operation.";
"errors.app.passepartout.connection_module_required" = "Routing module can only be enabled together with a connection.";
"errors.app.passepartout.corrupt_provider_module" = "Unable to connect to provider server (reason=%@).";
-"errors.app.passepartout.invalid_fields" = "Invalid fields (%@).";
+"errors.app.passepartout.invalid_fields" = "Invalid fields.";
"errors.app.passepartout.incompatible_modules" = "Some active modules are incompatible, try to only activate one of them.";
"errors.app.passepartout.parsing" = "Unable to parse file.";
"errors.app.passepartout.default" = "Unable to complete operation (code=%@).";
diff --git a/Passepartout/Library/Sources/AppUI/Views/App/ProfileImporterModifier.swift b/Passepartout/Library/Sources/AppUI/Views/App/ProfileImporterModifier.swift
index 2028c197..8ece598d 100644
--- a/Passepartout/Library/Sources/AppUI/Views/App/ProfileImporterModifier.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/App/ProfileImporterModifier.swift
@@ -70,7 +70,7 @@ private extension ProfileImporterModifier {
Strings.Placeholders.secret,
text: $importer.currentPassphrase
)
- Button(Strings.Views.Profiles.Alerts.Import.Passphrase.ok) {
+ Button(Strings.Alerts.Import.Passphrase.ok) {
Task {
try await importer.reImport(
url: url,
@@ -85,7 +85,7 @@ private extension ProfileImporterModifier {
}
func message(for url: URL) -> some View {
- Text(Strings.Views.Profiles.Alerts.Import.Passphrase.message(url.lastPathComponent))
+ Text(Strings.Alerts.Import.Passphrase.message(url.lastPathComponent))
}
func handleResult(_ result: Result<[URL], Error>) {
diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Configuration.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Configuration.swift
new file mode 100644
index 00000000..bf3ce765
--- /dev/null
+++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView+Configuration.swift
@@ -0,0 +1,272 @@
+//
+// OpenVPNView+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 OpenVPNView {
+ struct ConfigurationView: View {
+ let isServerPushed: Bool
+
+ let configuration: OpenVPN.Configuration.Builder
+
+ let credentialsRoute: any Hashable
+
+ var body: some View {
+ moduleSection(for: accountRows, header: Strings.Global.account)
+ moduleSection(for: remotesRows, header: Strings.Modules.Openvpn.remotes)
+ if !isServerPushed {
+ moduleSection(for: pullRows, header: Strings.Modules.Openvpn.pull)
+ }
+ moduleSection(for: redirectRows, header: Strings.Modules.Openvpn.redirectGateway)
+ moduleSection(
+ for: ipRows(for: configuration.ipv4, routes: configuration.routes4),
+ header: Strings.Unlocalized.ipv4
+ )
+ moduleSection(
+ for: ipRows(for: configuration.ipv6, routes: configuration.routes6),
+ header: Strings.Unlocalized.ipv6
+ )
+ moduleSection(for: dnsRows, header: Strings.Unlocalized.dns)
+ moduleSection(for: proxyRows, header: Strings.Unlocalized.proxy)
+ moduleSection(for: communicationRows, header: Strings.Modules.Openvpn.communication)
+ moduleSection(for: compressionRows, header: Strings.Modules.Openvpn.compression)
+ if !isServerPushed {
+ moduleSection(for: tlsRows, header: Strings.Unlocalized.tls)
+ }
+ moduleSection(for: otherRows, header: Strings.Global.other)
+ }
+ }
+}
+
+private extension OpenVPNView.ConfigurationView {
+ var accountRows: [ModuleRow]? {
+ guard configuration.authUserPass == true else {
+ return nil
+ }
+ return [.push(
+ caption: Strings.Modules.Openvpn.credentials,
+ route: HashableRoute(credentialsRoute))
+ ]
+ }
+
+ var remotesRows: [ModuleRow]? {
+ configuration.remotes?.map {
+ .copiableText(
+ value: "\($0.address.rawValue) → \($0.proto.socketType.rawValue):\($0.proto.port)"
+ )
+ }
+ .nilIfEmpty
+ }
+
+ var pullRows: [ModuleRow]? {
+ configuration.pullMask?.map {
+ .text(caption: $0.localizedDescription, value: nil)
+ }
+ .nilIfEmpty
+ }
+
+ func ipRows(for ip: IPSettings?, routes: [Route]?) -> [ModuleRow]? {
+ var rows: [ModuleRow] = []
+ if let ip {
+ ip.localizedDescription(optionalStyle: .address).map {
+ rows.append(.copiableText(caption: Strings.Global.address, value: $0))
+ }
+ ip.localizedDescription(optionalStyle: .defaultGateway).map {
+ rows.append(.copiableText(caption: Strings.Global.gateway, value: $0))
+ }
+
+ ip.includedRoutes
+ .filter { !$0.isDefault }
+ .nilIfEmpty
+ .map {
+ rows.append(.textList(
+ caption: Strings.Modules.Ip.Routes.included,
+ values: $0.map(\.localizedDescription)
+ ))
+ }
+
+ ip.excludedRoutes
+ .nilIfEmpty
+ .map {
+ rows.append(.textList(
+ caption: Strings.Modules.Ip.Routes.excluded,
+ values: $0.map(\.localizedDescription)
+ ))
+ }
+ }
+ routes?.forEach {
+ rows.append(.longContent(caption: Strings.Global.route, value: $0.localizedDescription))
+ }
+ return rows.nilIfEmpty
+ }
+
+ var redirectRows: [ModuleRow]? {
+ configuration.routingPolicies?
+ .compactMap {
+ switch $0 {
+ case .IPv4:
+ return .text(caption: Strings.Unlocalized.ipv4)
+
+ case .IPv6:
+ return .text(caption: Strings.Unlocalized.ipv6)
+
+ default:
+ return nil
+ }
+ }
+ .nilIfEmpty
+ }
+
+ var dnsRows: [ModuleRow]? {
+ var rows: [ModuleRow] = []
+
+ configuration.dnsServers?
+ .nilIfEmpty
+ .map {
+ rows.append(.textList(
+ caption: Strings.Global.servers,
+ values: $0
+ ))
+ }
+
+ configuration.dnsDomain.map {
+ rows.append(.copiableText(
+ caption: Strings.Global.domain,
+ value: $0
+ ))
+ }
+
+ configuration.searchDomains?
+ .nilIfEmpty
+ .map {
+ rows.append(.textList(
+ caption: Strings.Entities.Dns.searchDomains,
+ values: $0
+ ))
+ }
+
+ return rows.nilIfEmpty
+ }
+
+ var proxyRows: [ModuleRow]? {
+ var rows: [ModuleRow] = []
+ configuration.httpProxy.map {
+ rows.append(.copiableText(
+ caption: Strings.Unlocalized.http,
+ value: $0.rawValue
+ ))
+ }
+ configuration.httpsProxy.map {
+ rows.append(.copiableText(
+ caption: Strings.Unlocalized.https,
+ value: $0.rawValue
+ ))
+ }
+ configuration.proxyAutoConfigurationURL.map {
+ rows.append(.copiableText(
+ caption: Strings.Unlocalized.pac,
+ value: $0.absoluteString
+ ))
+ }
+ configuration.proxyBypassDomains?
+ .nilIfEmpty
+ .map {
+ rows.append(.textList(
+ caption: Strings.Entities.HttpProxy.bypassDomains,
+ values: $0
+ ))
+ }
+ return rows.nilIfEmpty
+ }
+
+ var communicationRows: [ModuleRow]? {
+ var rows: [ModuleRow] = []
+ configuration.cipher.map {
+ rows.append(.text(caption: Strings.Modules.Openvpn.cipher, value: $0.localizedDescription))
+ }
+ configuration.digest.map {
+ rows.append(.text(caption: Strings.Modules.Openvpn.digest, value: $0.localizedDescription))
+ }
+ if let xorMethod = configuration.xorMethod {
+ rows.append(.longContentPreview(
+ caption: Strings.Unlocalized.xor,
+ value: xorMethod.localizedDescription(style: .long),
+ preview: xorMethod.localizedDescription(style: .short)
+ ))
+ }
+ return rows.nilIfEmpty
+ }
+
+ var compressionRows: [ModuleRow]? {
+ var rows: [ModuleRow] = []
+ configuration.compressionFraming.map {
+ rows.append(.text(caption: Strings.Modules.Openvpn.compressionFraming, value: $0.localizedDescription))
+ }
+ configuration.compressionAlgorithm.map {
+ rows.append(.text(caption: Strings.Modules.Openvpn.compressionAlgorithm, value: $0.localizedDescription))
+ }
+ return rows.nilIfEmpty
+ }
+
+ var tlsRows: [ModuleRow]? {
+ var rows: [ModuleRow] = []
+ configuration.ca.map {
+ rows.append(.longContentPreview(caption: Strings.Unlocalized.ca, value: $0.pem, preview: nil))
+ }
+ configuration.clientCertificate.map {
+ rows.append(.longContentPreview(caption: Strings.Global.certificate, value: $0.pem, preview: nil))
+ }
+ configuration.clientKey.map {
+ rows.append(.longContentPreview(caption: Strings.Global.key, value: $0.pem, preview: nil))
+ }
+ configuration.tlsWrap.map {
+ rows.append(.longContentPreview(
+ caption: Strings.Modules.Openvpn.tlsWrap,
+ value: $0.key.hexString,
+ preview: configuration.localizedDescription(style: .tlsWrap)
+ ))
+ }
+ rows.append(.text(caption: Strings.Modules.Openvpn.eku, value: configuration.localizedDescription(style: .eku)))
+ return rows.nilIfEmpty
+ }
+
+ var otherRows: [ModuleRow]? {
+ var rows: [ModuleRow] = []
+ configuration.localizedDescription(optionalStyle: .keepAlive).map {
+ rows.append(.text(caption: Strings.Global.keepAlive, value: $0))
+ }
+ configuration.localizedDescription(optionalStyle: .renegotiatesAfter).map {
+ rows.append(.text(caption: Strings.Modules.Openvpn.renegotiation, value: $0))
+ }
+ configuration.localizedDescription(optionalStyle: .randomizeEndpoint).map {
+ rows.append(.text(caption: Strings.Modules.Openvpn.randomizeEndpoint, value: $0))
+ }
+ configuration.localizedDescription(optionalStyle: .randomizeHostnames).map {
+ rows.append(.text(caption: Strings.Modules.Openvpn.randomizeHostname, value: $0))
+ }
+ return rows.nilIfEmpty
+ }
+}
diff --git a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift
index 98a02228..7e5472d1 100644
--- a/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/Modules/OpenVPNView.swift
@@ -23,8 +23,10 @@
// along with Passepartout. If not, see .
//
+import CPassepartoutOpenVPNOpenSSL
import PassepartoutKit
import SwiftUI
+import UtilsLibrary
struct OpenVPNView: View, ModuleDraftEditing {
@@ -38,12 +40,28 @@ struct OpenVPNView: View, ModuleDraftEditing {
private let isServerPushed: Bool
+ @State
+ private var isImporting = false
+
+ @State
+ private var importURL: URL?
+
+ @State
+ private var importPassphrase: String?
+
+ @State
+ private var requiresPassphrase = false
+
@State
private var paywallReason: PaywallReason?
+ @StateObject
+ private var errorHandler: ErrorHandler = .default()
+
init(serverConfiguration: OpenVPN.Configuration) {
let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder())
let editor = ProfileEditor(modules: [module])
+ assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil")
self.editor = editor
self.module = module
@@ -59,7 +77,13 @@ struct OpenVPNView: View, ModuleDraftEditing {
var body: some View {
contentView
.moduleView(editor: editor, draft: draft.wrappedValue, withName: !isServerPushed)
+ .fileImporter(
+ isPresented: $isImporting,
+ allowedContentTypes: [.item],
+ onCompletion: importConfiguration
+ )
.modifier(PaywallModifier(reason: $paywallReason))
+ .withErrorHandler(errorHandler)
.navigationDestination(for: Subroute.self, destination: destination)
}
}
@@ -67,25 +91,54 @@ struct OpenVPNView: View, ModuleDraftEditing {
// MARK: - Content
private extension OpenVPNView {
- var configuration: OpenVPN.Configuration.Builder {
- draft.wrappedValue.configurationBuilder ?? .init(withFallbacks: true)
- }
@ViewBuilder
var contentView: some View {
- if isServerPushed || draft.wrappedValue.configurationBuilder != nil {
- manualView
+ if let configuration = draft.wrappedValue.configurationBuilder {
+ ConfigurationView(
+ isServerPushed: isServerPushed,
+ configuration: configuration,
+ credentialsRoute: Subroute.credentials
+ )
} else {
- manualView
+ importView
.modifier(providerModifier)
}
}
+ @ViewBuilder
+ var importView: some View {
+ if providerId.wrappedValue == nil {
+ Button(Strings.Modules.General.Rows.importFromFile) {
+ isImporting = true
+ }
+ .alert(
+ module.typeDescription,
+ isPresented: $requiresPassphrase,
+ presenting: importURL,
+ actions: { url in
+ SecureField(
+ Strings.Placeholders.secret,
+ text: $importPassphrase ?? ""
+ )
+ Button(Strings.Alerts.Import.Passphrase.ok) {
+ importConfiguration(from: .success(url))
+ }
+ Button(Strings.Global.cancel, role: .cancel) {
+ isImporting = false
+ }
+ },
+ message: {
+ Text(Strings.Alerts.Import.Passphrase.message($0.lastPathComponent))
+ }
+ )
+ }
+ }
+
var providerModifier: some ViewModifier {
VPNProviderContentModifier(
providerId: providerId,
selectedEntity: providerEntity,
- isRequired: true,
paywallReason: $paywallReason,
entityDestination: Subroute.providerServer,
providerRows: {
@@ -113,8 +166,36 @@ private extension OpenVPNView {
path.wrappedValue.removeLast()
}
- func importConfiguration(from url: URL) {
- // TODO: #657, import draft from external URL
+ func importConfiguration(from result: Result) {
+ do {
+ let url = try result.get()
+ guard url.startAccessingSecurityScopedResource() else {
+ throw AppError.permissionDenied
+ }
+ defer {
+ url.stopAccessingSecurityScopedResource()
+ }
+ importURL = url
+
+ let parsed = try StandardOpenVPNParser(decrypter: OSSLTLSBox())
+ .parsed(fromURL: url, passphrase: importPassphrase)
+
+ draft.wrappedValue.configurationBuilder = parsed.configuration.builder()
+ } catch StandardOpenVPNParserError.encryptionPassphrase,
+ StandardOpenVPNParserError.unableToDecrypt {
+ Task {
+ // XXX: re-present same alert after artificial delay
+ try? await Task.sleep(for: .milliseconds(500))
+ importPassphrase = nil
+ requiresPassphrase = true
+ }
+ } catch {
+ pp_log(.app, .error, "Unable to import OpenVPN configuration: \(error)")
+ errorHandler.handle(
+ (error as? StandardOpenVPNParserError)?.asPassepartoutError ?? error,
+ title: module.typeDescription
+ )
+ }
}
}
@@ -152,242 +233,6 @@ private extension OpenVPNView {
}
}
-// MARK: - Manual configuration
-
-private extension OpenVPNView {
-
- @ViewBuilder
- var manualView: some View {
- moduleSection(for: accountRows, header: Strings.Global.account)
- moduleSection(for: remotesRows, header: Strings.Modules.Openvpn.remotes)
- if !isServerPushed {
- moduleSection(for: pullRows, header: Strings.Modules.Openvpn.pull)
- }
- moduleSection(for: redirectRows, header: Strings.Modules.Openvpn.redirectGateway)
- moduleSection(
- for: ipRows(for: configuration.ipv4, routes: configuration.routes4),
- header: Strings.Unlocalized.ipv4
- )
- moduleSection(
- for: ipRows(for: configuration.ipv6, routes: configuration.routes6),
- header: Strings.Unlocalized.ipv6
- )
- moduleSection(for: dnsRows, header: Strings.Unlocalized.dns)
- moduleSection(for: proxyRows, header: Strings.Unlocalized.proxy)
- moduleSection(for: communicationRows, header: Strings.Modules.Openvpn.communication)
- moduleSection(for: compressionRows, header: Strings.Modules.Openvpn.compression)
- if !isServerPushed {
- moduleSection(for: tlsRows, header: Strings.Unlocalized.tls)
- }
- moduleSection(for: otherRows, header: Strings.Global.other)
- }
-
- var accountRows: [ModuleRow]? {
- guard configuration.authUserPass == true else {
- return nil
- }
- return [.push(caption: Strings.Modules.Openvpn.credentials, route: HashableRoute(Subroute.credentials))]
- }
-
- var remotesRows: [ModuleRow]? {
- configuration.remotes?.map {
- .copiableText(
- value: "\($0.address.rawValue) → \($0.proto.socketType.rawValue):\($0.proto.port)"
- )
- }
- .nilIfEmpty
- }
-
- var pullRows: [ModuleRow]? {
- configuration.pullMask?.map {
- .text(caption: $0.localizedDescription, value: nil)
- }
- .nilIfEmpty
- }
-
- func ipRows(for ip: IPSettings?, routes: [Route]?) -> [ModuleRow]? {
- var rows: [ModuleRow] = []
- if let ip {
- ip.localizedDescription(optionalStyle: .address).map {
- rows.append(.copiableText(caption: Strings.Global.address, value: $0))
- }
- ip.localizedDescription(optionalStyle: .defaultGateway).map {
- rows.append(.copiableText(caption: Strings.Global.gateway, value: $0))
- }
-
- ip.includedRoutes
- .filter { !$0.isDefault }
- .nilIfEmpty
- .map {
- rows.append(.textList(
- caption: Strings.Modules.Ip.Routes.included,
- values: $0.map(\.localizedDescription)
- ))
- }
-
- ip.excludedRoutes
- .nilIfEmpty
- .map {
- rows.append(.textList(
- caption: Strings.Modules.Ip.Routes.excluded,
- values: $0.map(\.localizedDescription)
- ))
- }
- }
- routes?.forEach {
- rows.append(.longContent(caption: Strings.Global.route, value: $0.localizedDescription))
- }
- return rows.nilIfEmpty
- }
-
- var redirectRows: [ModuleRow]? {
- configuration.routingPolicies?
- .compactMap {
- switch $0 {
- case .IPv4:
- return .text(caption: Strings.Unlocalized.ipv4)
-
- case .IPv6:
- return .text(caption: Strings.Unlocalized.ipv6)
-
- default:
- return nil
- }
- }
- .nilIfEmpty
- }
-
- var dnsRows: [ModuleRow]? {
- var rows: [ModuleRow] = []
-
- configuration.dnsServers?
- .nilIfEmpty
- .map {
- rows.append(.textList(
- caption: Strings.Global.servers,
- values: $0
- ))
- }
-
- configuration.dnsDomain.map {
- rows.append(.copiableText(
- caption: Strings.Global.domain,
- value: $0
- ))
- }
-
- configuration.searchDomains?
- .nilIfEmpty
- .map {
- rows.append(.textList(
- caption: Strings.Entities.Dns.searchDomains,
- values: $0
- ))
- }
-
- return rows.nilIfEmpty
- }
-
- var proxyRows: [ModuleRow]? {
- var rows: [ModuleRow] = []
- configuration.httpProxy.map {
- rows.append(.copiableText(
- caption: Strings.Unlocalized.http,
- value: $0.rawValue
- ))
- }
- configuration.httpsProxy.map {
- rows.append(.copiableText(
- caption: Strings.Unlocalized.https,
- value: $0.rawValue
- ))
- }
- configuration.proxyAutoConfigurationURL.map {
- rows.append(.copiableText(
- caption: Strings.Unlocalized.pac,
- value: $0.absoluteString
- ))
- }
- configuration.proxyBypassDomains?
- .nilIfEmpty
- .map {
- rows.append(.textList(
- caption: Strings.Entities.HttpProxy.bypassDomains,
- values: $0
- ))
- }
- return rows.nilIfEmpty
- }
-
- var communicationRows: [ModuleRow]? {
- var rows: [ModuleRow] = []
- configuration.cipher.map {
- rows.append(.text(caption: Strings.Modules.Openvpn.cipher, value: $0.localizedDescription))
- }
- configuration.digest.map {
- rows.append(.text(caption: Strings.Modules.Openvpn.digest, value: $0.localizedDescription))
- }
- if let xorMethod = configuration.xorMethod {
- rows.append(.longContentPreview(
- caption: Strings.Unlocalized.xor,
- value: xorMethod.localizedDescription(style: .long),
- preview: xorMethod.localizedDescription(style: .short)
- ))
- }
- return rows.nilIfEmpty
- }
-
- var compressionRows: [ModuleRow]? {
- var rows: [ModuleRow] = []
- configuration.compressionFraming.map {
- rows.append(.text(caption: Strings.Modules.Openvpn.compressionFraming, value: $0.localizedDescription))
- }
- configuration.compressionAlgorithm.map {
- rows.append(.text(caption: Strings.Modules.Openvpn.compressionAlgorithm, value: $0.localizedDescription))
- }
- return rows.nilIfEmpty
- }
-
- var tlsRows: [ModuleRow]? {
- var rows: [ModuleRow] = []
- configuration.ca.map {
- rows.append(.longContentPreview(caption: Strings.Unlocalized.ca, value: $0.pem, preview: nil))
- }
- configuration.clientCertificate.map {
- rows.append(.longContentPreview(caption: Strings.Global.certificate, value: $0.pem, preview: nil))
- }
- configuration.clientKey.map {
- rows.append(.longContentPreview(caption: Strings.Global.key, value: $0.pem, preview: nil))
- }
- configuration.tlsWrap.map {
- rows.append(.longContentPreview(
- caption: Strings.Modules.Openvpn.tlsWrap,
- value: $0.key.hexString,
- preview: configuration.localizedDescription(style: .tlsWrap)
- ))
- }
- rows.append(.text(caption: Strings.Modules.Openvpn.eku, value: configuration.localizedDescription(style: .eku)))
- return rows.nilIfEmpty
- }
-
- var otherRows: [ModuleRow]? {
- var rows: [ModuleRow] = []
- configuration.localizedDescription(optionalStyle: .keepAlive).map {
- rows.append(.text(caption: Strings.Global.keepAlive, value: $0))
- }
- configuration.localizedDescription(optionalStyle: .renegotiatesAfter).map {
- rows.append(.text(caption: Strings.Modules.Openvpn.renegotiation, value: $0))
- }
- configuration.localizedDescription(optionalStyle: .randomizeEndpoint).map {
- rows.append(.text(caption: Strings.Modules.Openvpn.randomizeEndpoint, value: $0))
- }
- configuration.localizedDescription(optionalStyle: .randomizeHostnames).map {
- rows.append(.text(caption: Strings.Modules.Openvpn.randomizeHostname, value: $0))
- }
- return rows.nilIfEmpty
- }
-}
-
// MARK: - Previews
// swiftlint: disable force_try
diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift
index b39d1ef6..2d8ffced 100644
--- a/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/Provider/ProviderContentModifier.swift
@@ -41,8 +41,6 @@ struct ProviderContentModifier: ViewModifier where Entity:
let entityType: Entity.Type
- let isRequired: Bool
-
@Binding
var paywallReason: PaywallReason?
@@ -64,9 +62,7 @@ struct ProviderContentModifier: ViewModifier where Entity:
}
.disabled(providerManager.isLoading)
- if providerId == nil && !isRequired {
- content
- }
+ content
}
static func == (lhs: Self, rhs: Self) -> Bool {
@@ -129,7 +125,7 @@ private extension ProviderContentModifier {
ProviderPicker(
providers: supportedProviders,
providerId: $providerId,
- isRequired: isRequired,
+ isRequired: true,
isLoading: providerManager.isLoading
)
}
@@ -221,7 +217,6 @@ private extension ProviderContentModifier {
apis: [API.bundled],
providerId: .constant(.hideme),
entityType: VPNEntity.self,
- isRequired: false,
paywallReason: .constant(nil),
providerRows: {},
onSelectProvider: { _, _, _ in }
diff --git a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift
index 3a2bbb62..7415f6d0 100644
--- a/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift
+++ b/Passepartout/Library/Sources/AppUI/Views/Provider/VPNProviderContentModifier.swift
@@ -28,7 +28,7 @@ import PassepartoutKit
import SwiftUI
import UtilsLibrary
-struct VPNProviderContentModifier: ViewModifier where Configuration: ProviderConfigurationIdentifiable & Codable, Destination: Hashable, ProviderRows: View {
+struct VPNProviderContentModifier: ViewModifier where Configuration: ProviderConfigurationIdentifiable & Codable, ProviderRows: View {
var apis: [APIMapper] = API.shared
@@ -38,12 +38,10 @@ struct VPNProviderContentModifier: Vie
@Binding
var selectedEntity: VPNEntity?
- let isRequired: Bool
-
@Binding
var paywallReason: PaywallReason?
- let entityDestination: Destination
+ let entityDestination: any Hashable
@ViewBuilder
let providerRows: ProviderRows
@@ -55,7 +53,6 @@ struct VPNProviderContentModifier: Vie
apis: apis,
providerId: $providerId,
entityType: VPNEntity.self,
- isRequired: isRequired,
paywallReason: $paywallReason,
providerRows: {
providerEntityRow
@@ -99,7 +96,6 @@ private extension VPNProviderContentModifier {
apis: [API.bundled],
providerId: .constant(.hideme),
selectedEntity: .constant(nil as VPNEntity?),
- isRequired: false,
paywallReason: .constant(nil),
entityDestination: "Destination",
providerRows: {