mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-02-18 22:02:11 +00:00
Move module destinations to standalone entities (#1166)
Decouple destinations from the module view so that one can navigate to them from any point of the app. Affects only OpenVPN for now. Preparation for "profile shortcuts".
This commit is contained in:
parent
41874c8bb1
commit
5059423bf3
@ -0,0 +1,147 @@
|
||||
//
|
||||
// OpenVPNModule+Destination.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 2/12/25.
|
||||
// Copyright (c) 2025 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 PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
extension OpenVPNView {
|
||||
enum Subroute: Hashable {
|
||||
case providerServer
|
||||
|
||||
case providerConfiguration(OpenVPN.Configuration)
|
||||
|
||||
case credentials
|
||||
|
||||
case editRemotes
|
||||
}
|
||||
}
|
||||
|
||||
extension OpenVPNModule.Builder: ModuleDestinationProviding {
|
||||
public func moduleDestination(with parameters: ModuleDestinationParameters, path: Binding<NavigationPath>) -> some ViewModifier {
|
||||
DestinationModifier(parameters: parameters, path: path)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DestinationModifier: ViewModifier {
|
||||
let parameters: ModuleDestinationParameters
|
||||
|
||||
@Binding
|
||||
var path: NavigationPath
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.navigationDestination(for: OpenVPNView.Subroute.self) {
|
||||
switch $0 {
|
||||
case .providerServer:
|
||||
draft.wrappedValue.providerSelection.map {
|
||||
ProviderServerView(
|
||||
moduleId: parameters.module.id,
|
||||
providerId: $0.id,
|
||||
selectedEntity: $0.entity,
|
||||
filtersWithSelection: true,
|
||||
onSelect: onSelectServer
|
||||
)
|
||||
}
|
||||
|
||||
case .providerConfiguration(let configuration):
|
||||
Form {
|
||||
OpenVPNView.ConfigurationView(
|
||||
isServerPushed: false,
|
||||
configuration: configuration.builder(),
|
||||
credentialsRoute: nil,
|
||||
remotesRoute: nil,
|
||||
excludedEndpoints: excludedEndpoints
|
||||
)
|
||||
}
|
||||
.themeForm()
|
||||
.navigationTitle(Strings.Global.Nouns.configuration)
|
||||
|
||||
case .credentials:
|
||||
Form {
|
||||
OpenVPNCredentialsView(
|
||||
profileEditor: parameters.editor,
|
||||
providerId: draft.wrappedValue.providerId,
|
||||
isInteractive: draft.isInteractive,
|
||||
credentials: draft.credentials
|
||||
)
|
||||
}
|
||||
.navigationTitle(Strings.Modules.Openvpn.credentials)
|
||||
.themeForm()
|
||||
.themeAnimation(on: draft.wrappedValue.isInteractive, category: .modules)
|
||||
|
||||
case .editRemotes:
|
||||
OpenVPNView.RemotesView(remotes: editableRemotesBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension DestinationModifier {
|
||||
var draft: Binding<OpenVPNModule.Builder> {
|
||||
guard let builder = parameters.module as? OpenVPNModule.Builder else {
|
||||
fatalError("Not a OpenVPNModule.Builder")
|
||||
}
|
||||
return parameters.editor[builder]
|
||||
}
|
||||
|
||||
var excludedEndpoints: ObservableList<ExtendedEndpoint> {
|
||||
parameters.editor.excludedEndpoints(for: parameters.module.id, preferences: parameters.preferences)
|
||||
}
|
||||
|
||||
var editableRemotesBinding: Binding<[String]> {
|
||||
Binding {
|
||||
draft.wrappedValue.configurationBuilder?.remotes?.map(\.rawValue) ?? []
|
||||
} set: {
|
||||
draft.wrappedValue.configurationBuilder?.remotes = $0.compactMap {
|
||||
ExtendedEndpoint(rawValue: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onSelectServer(server: ProviderServer, preset: ProviderPreset<OpenVPNProviderTemplate>) {
|
||||
draft.wrappedValue.providerEntity = ProviderEntity(server: server, preset: preset)
|
||||
resetExcludedEndpointsWithCurrentProviderEntity()
|
||||
path.removeLast()
|
||||
}
|
||||
|
||||
// filter out exclusions unrelated to current server
|
||||
func resetExcludedEndpointsWithCurrentProviderEntity() {
|
||||
do {
|
||||
let cfg = try draft.wrappedValue.providerSelection?.configuration()
|
||||
parameters.editor.profile.attributes.editPreferences(inModule: parameters.module.id) {
|
||||
if let cfg {
|
||||
$0.excludedEndpoints = Set(cfg.remotes?.filter {
|
||||
parameters.preferences.isExcludedEndpoint($0)
|
||||
} ?? [])
|
||||
} else {
|
||||
$0.excludedEndpoints = []
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to build provider configuration for excluded endpoints: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,6 @@
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonUtils
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
@ -36,3 +35,67 @@ extension OpenVPNModule.Builder: ModuleViewProviding {
|
||||
|
||||
extension OpenVPNModule: ProviderServerCoordinatorSupporting {
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
// swiftlint: disable force_try
|
||||
extension OpenVPN.Configuration.Builder {
|
||||
static var forPreviews: Self {
|
||||
var builder = OpenVPN.Configuration.Builder(withFallbacks: true)
|
||||
builder.noPullMask = [.proxy]
|
||||
builder.authUserPass = true
|
||||
builder.remotes = [
|
||||
.init(rawValue: "2.2.2.2:UDP:2222")!,
|
||||
.init(rawValue: "6.6.6.6:UDP:6666")!,
|
||||
.init(rawValue: "12.12.12.12:TCP:21212")!,
|
||||
.init(rawValue: "12:12:12:12:20:20:20:20:TCP6:21212")!
|
||||
]
|
||||
builder.ipv4 = IPSettings(subnet: try! .init("5.5.5.5", 24))
|
||||
.including(routes: [
|
||||
.init(defaultWithGateway: .ip("120.1.1.1", .v4)),
|
||||
.init(.init(rawValue: "55.10.20.30/32"), nil)
|
||||
])
|
||||
.excluding(routes: [
|
||||
.init(.init(rawValue: "88.40.30.30/32"), nil),
|
||||
.init(.init(rawValue: "60.60.60.60/32"), .ip("127.0.0.1", .v4))
|
||||
])
|
||||
builder.ipv6 = IPSettings(subnet: try! .init("::5", 24))
|
||||
.including(routes: [
|
||||
.init(defaultWithGateway: .ip("120::1:1:1", .v6)),
|
||||
.init(.init(rawValue: "55:10:20::30/128"), nil),
|
||||
.init(.init(rawValue: "60:60:60::60/128"), .ip("::2", .v6))
|
||||
])
|
||||
.excluding(routes: [
|
||||
.init(.init(rawValue: "88:40:30::30/32"), nil)
|
||||
])
|
||||
builder.routingPolicies = [.IPv4, .IPv6]
|
||||
builder.dnsServers = ["1.2.3.4", "4.5.6.7"]
|
||||
builder.dnsDomain = "domain.com"
|
||||
builder.searchDomains = ["search1.com", "search2.com"]
|
||||
builder.httpProxy = try! .init("10.10.10.10", 1080)
|
||||
builder.httpsProxy = try! .init("10.10.10.10", 8080)
|
||||
builder.proxyAutoConfigurationURL = URL(string: "https://hello.pac")!
|
||||
builder.proxyBypassDomains = ["bypass1.com", "bypass2.com"]
|
||||
builder.xorMethod = .xormask(mask: .init(Data(hex: "1234")))
|
||||
builder.ca = .init(mockPem: "ca-certificate")
|
||||
builder.clientCertificate = .init(mockPem: "client-certificate")
|
||||
builder.clientKey = .init(mockPem: "client-key")
|
||||
builder.tlsWrap = .init(strategy: .auth, key: .init(biData: Data(count: 256)))
|
||||
builder.keepAliveInterval = 10.0
|
||||
builder.renegotiatesAfter = 60.0
|
||||
builder.randomizeEndpoint = true
|
||||
builder.randomizeHostnames = true
|
||||
return builder
|
||||
}
|
||||
}
|
||||
// swiftlint: enable force_try
|
||||
|
||||
private extension OpenVPN.CryptoContainer {
|
||||
init(mockPem: String) {
|
||||
self.init(pem: """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
\(mockPem)
|
||||
-----END CERTIFICATE-----
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
//
|
||||
// OpenVPNView+Extensions.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 12/8/24.
|
||||
// Copyright (c) 2025 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
|
||||
|
||||
// swiftlint: disable force_try
|
||||
extension OpenVPN.Configuration.Builder {
|
||||
static var forPreviews: Self {
|
||||
var builder = OpenVPN.Configuration.Builder(withFallbacks: true)
|
||||
builder.noPullMask = [.proxy]
|
||||
builder.authUserPass = true
|
||||
builder.remotes = [
|
||||
.init(rawValue: "2.2.2.2:UDP:2222")!,
|
||||
.init(rawValue: "6.6.6.6:UDP:6666")!,
|
||||
.init(rawValue: "12.12.12.12:TCP:21212")!,
|
||||
.init(rawValue: "12:12:12:12:20:20:20:20:TCP6:21212")!
|
||||
]
|
||||
builder.ipv4 = IPSettings(subnet: try! .init("5.5.5.5", 24))
|
||||
.including(routes: [
|
||||
.init(defaultWithGateway: .ip("120.1.1.1", .v4)),
|
||||
.init(.init(rawValue: "55.10.20.30/32"), nil)
|
||||
])
|
||||
.excluding(routes: [
|
||||
.init(.init(rawValue: "88.40.30.30/32"), nil),
|
||||
.init(.init(rawValue: "60.60.60.60/32"), .ip("127.0.0.1", .v4))
|
||||
])
|
||||
builder.ipv6 = IPSettings(subnet: try! .init("::5", 24))
|
||||
.including(routes: [
|
||||
.init(defaultWithGateway: .ip("120::1:1:1", .v6)),
|
||||
.init(.init(rawValue: "55:10:20::30/128"), nil),
|
||||
.init(.init(rawValue: "60:60:60::60/128"), .ip("::2", .v6))
|
||||
])
|
||||
.excluding(routes: [
|
||||
.init(.init(rawValue: "88:40:30::30/32"), nil)
|
||||
])
|
||||
builder.routingPolicies = [.IPv4, .IPv6]
|
||||
builder.dnsServers = ["1.2.3.4", "4.5.6.7"]
|
||||
builder.dnsDomain = "domain.com"
|
||||
builder.searchDomains = ["search1.com", "search2.com"]
|
||||
builder.httpProxy = try! .init("10.10.10.10", 1080)
|
||||
builder.httpsProxy = try! .init("10.10.10.10", 8080)
|
||||
builder.proxyAutoConfigurationURL = URL(string: "https://hello.pac")!
|
||||
builder.proxyBypassDomains = ["bypass1.com", "bypass2.com"]
|
||||
builder.xorMethod = .xormask(mask: .init(Data(hex: "1234")))
|
||||
builder.ca = .init(mockPem: "ca-certificate")
|
||||
builder.clientCertificate = .init(mockPem: "client-certificate")
|
||||
builder.clientKey = .init(mockPem: "client-key")
|
||||
builder.tlsWrap = .init(strategy: .auth, key: .init(biData: Data(count: 256)))
|
||||
builder.keepAliveInterval = 10.0
|
||||
builder.renegotiatesAfter = 60.0
|
||||
builder.randomizeEndpoint = true
|
||||
builder.randomizeHostnames = true
|
||||
return builder
|
||||
}
|
||||
}
|
||||
// swiftlint: enable force_try
|
||||
|
||||
private extension OpenVPN.CryptoContainer {
|
||||
init(mockPem: String) {
|
||||
self.init(pem: """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
\(mockPem)
|
||||
-----END CERTIFICATE-----
|
||||
""")
|
||||
}
|
||||
}
|
@ -80,7 +80,15 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
||||
isImporting: $isImporting,
|
||||
errorHandler: errorHandler
|
||||
))
|
||||
.navigationDestination(for: Subroute.self, destination: destination)
|
||||
.modifier(module.moduleDestination(
|
||||
with: .init(
|
||||
editor: editor,
|
||||
module: module,
|
||||
preferences: modulePreferences,
|
||||
impl: impl
|
||||
),
|
||||
path: path
|
||||
))
|
||||
.themeAnimation(on: draft.wrappedValue.providerId, category: .modules)
|
||||
.modifier(PaywallModifier(reason: $paywallReason))
|
||||
.withErrorHandler(errorHandler)
|
||||
@ -144,105 +152,10 @@ private extension OpenVPNView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Destinations
|
||||
|
||||
private extension OpenVPNView {
|
||||
enum Subroute: Hashable {
|
||||
case providerServer
|
||||
|
||||
case providerConfiguration(OpenVPN.Configuration)
|
||||
|
||||
case credentials
|
||||
|
||||
case editRemotes
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func destination(for route: Subroute) -> some View {
|
||||
switch route {
|
||||
case .providerServer:
|
||||
draft.wrappedValue.providerSelection.map {
|
||||
ProviderServerView(
|
||||
moduleId: module.id,
|
||||
providerId: $0.id,
|
||||
selectedEntity: $0.entity,
|
||||
filtersWithSelection: true,
|
||||
onSelect: onSelectServer
|
||||
)
|
||||
}
|
||||
|
||||
case .providerConfiguration(let configuration):
|
||||
Form {
|
||||
ConfigurationView(
|
||||
isServerPushed: false,
|
||||
configuration: configuration.builder(),
|
||||
credentialsRoute: nil,
|
||||
remotesRoute: nil,
|
||||
excludedEndpoints: excludedEndpoints
|
||||
)
|
||||
}
|
||||
.themeForm()
|
||||
.navigationTitle(Strings.Global.Nouns.configuration)
|
||||
|
||||
case .credentials:
|
||||
Form {
|
||||
OpenVPNCredentialsView(
|
||||
profileEditor: editor,
|
||||
providerId: draft.wrappedValue.providerId,
|
||||
isInteractive: draft.isInteractive,
|
||||
credentials: draft.credentials
|
||||
)
|
||||
}
|
||||
.navigationTitle(Strings.Modules.Openvpn.credentials)
|
||||
.themeForm()
|
||||
.themeAnimation(on: draft.wrappedValue.isInteractive, category: .modules)
|
||||
|
||||
case .editRemotes:
|
||||
RemotesView(remotes: editableRemotesBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Logic
|
||||
|
||||
private extension OpenVPNView {
|
||||
var excludedEndpoints: ObservableList<ExtendedEndpoint> {
|
||||
editor.excludedEndpoints(for: module.id, preferences: modulePreferences)
|
||||
}
|
||||
|
||||
var editableRemotesBinding: Binding<[String]> {
|
||||
Binding {
|
||||
draft.wrappedValue.configurationBuilder?.remotes?.map(\.rawValue) ?? []
|
||||
} set: {
|
||||
draft.wrappedValue.configurationBuilder?.remotes = $0.compactMap {
|
||||
ExtendedEndpoint(rawValue: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onSelectServer(server: ProviderServer, preset: ProviderPreset<OpenVPNProviderTemplate>) {
|
||||
draft.wrappedValue.providerEntity = ProviderEntity(server: server, preset: preset)
|
||||
resetExcludedEndpointsWithCurrentProviderEntity()
|
||||
path.wrappedValue.removeLast()
|
||||
}
|
||||
|
||||
// filter out exclusions unrelated to current server
|
||||
func resetExcludedEndpointsWithCurrentProviderEntity() {
|
||||
do {
|
||||
let cfg = try draft.wrappedValue.providerSelection?.configuration()
|
||||
editor.profile.attributes.editPreferences(inModule: module.id) {
|
||||
if let cfg {
|
||||
$0.excludedEndpoints = Set(cfg.remotes?.filter {
|
||||
modulePreferences.isExcludedEndpoint($0)
|
||||
} ?? [])
|
||||
} else {
|
||||
$0.excludedEndpoints = []
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
pp_log(.app, .error, "Unable to build provider configuration for excluded endpoints: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
@ -0,0 +1,58 @@
|
||||
//
|
||||
// ModuleDestinationProviding.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 2/12/25.
|
||||
// Copyright (c) 2025 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 CommonLibrary
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
public protocol ModuleDestinationProviding {
|
||||
associatedtype Destination: ViewModifier
|
||||
|
||||
@MainActor
|
||||
func moduleDestination(with parameters: ModuleDestinationParameters, path: Binding<NavigationPath>) -> Destination
|
||||
}
|
||||
|
||||
public struct ModuleDestinationParameters {
|
||||
public let editor: ProfileEditor
|
||||
|
||||
public let module: any ModuleBuilder
|
||||
|
||||
public let preferences: ModulePreferences
|
||||
|
||||
public let impl: (any ModuleImplementation)?
|
||||
|
||||
@MainActor
|
||||
public init(
|
||||
editor: ProfileEditor,
|
||||
module: any ModuleBuilder,
|
||||
preferences: ModulePreferences,
|
||||
impl: (any ModuleImplementation)?
|
||||
) {
|
||||
self.editor = editor
|
||||
self.module = module
|
||||
self.preferences = preferences
|
||||
self.impl = impl
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user