passepartout-apple/Passepartout/App/Views/EndpointView+OpenVPN.swift

294 lines
9.2 KiB
Swift
Raw Normal View History

2022-04-12 13:09:14 +00:00
//
// EndpointView+OpenVPN.swift
// Passepartout
//
// Created by Davide De Rosa on 2/19/22.
2023-03-17 15:56:19 +00:00
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
2022-04-12 13:09:14 +00:00
//
// 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/>.
//
2022-06-23 21:31:01 +00:00
import PassepartoutLibrary
import SwiftUI
2022-04-12 13:09:14 +00:00
import TunnelKitOpenVPN
extension EndpointView {
struct OpenVPNView: View {
@Environment(\.presentationMode) private var presentationMode
@ObservedObject private var providerManager: ProviderManager
2022-04-12 13:09:14 +00:00
@ObservedObject private var currentProfile: ObservableProfile
@Binding private var builder: OpenVPN.ConfigurationBuilder
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
@Binding private var customEndpoint: Endpoint?
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
@State private var isFirstAppearance = true
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
@State private var isAutomatic = false
@State private var selectedSocketType: SocketType = .udp
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
@State private var selectedPort: UInt16 = 0
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
init(currentProfile: ObservableProfile) {
let providerManager: ProviderManager = .shared
2022-04-12 13:09:14 +00:00
self.providerManager = providerManager
self.currentProfile = currentProfile
_builder = currentProfile.builderBinding(providerManager: providerManager)
_customEndpoint = currentProfile.customEndpointBinding
2022-04-12 13:09:14 +00:00
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var body: some View {
ScrollViewReader { scrollProxy in
List {
mainSection
if !isAutomatic {
filtersSection
addressesSection
}
advancedSection
}.onAppear {
scrollToCustomEndpoint(scrollProxy)
preselectFilters(once: true)
}.onChange(of: isAutomatic, perform: onToggleAutomatic)
.onChange(of: selectedSocketType, perform: preselectPort)
.onChange(of: customEndpoint) { _ in
withAnimation {
preselectFilters(once: false)
}
}
}.navigationTitle(L10n.Global.Strings.endpoint)
}
}
}
// MARK: -
private extension EndpointView.OpenVPNView {
var mainSection: some View {
2022-04-12 13:09:14 +00:00
Section {
2022-04-23 10:08:24 +00:00
Toggle(L10n.Global.Strings.automatic, isOn: $isAutomatic.themeAnimation())
2022-04-12 13:09:14 +00:00
}
}
2023-03-17 20:55:47 +00:00
var filtersSection: some View {
2022-04-12 13:09:14 +00:00
Section {
themeTextPicker(
L10n.Global.Strings.protocol,
selection: $selectedSocketType,
values: availableSocketTypes,
2022-04-12 13:09:14 +00:00
description: \.rawValue
)
themeTextPicker(
L10n.Global.Strings.port,
selection: $selectedPort,
values: allPorts(forSocketType: selectedSocketType),
description: \.description
)
}
}
var addressesSection: some View {
Section {
2022-04-12 13:09:14 +00:00
filteredRemotes.map {
ForEach($0, content: button(forEndpoint:))
}
} header: {
Text(L10n.Global.Strings.addresses)
2022-04-12 13:09:14 +00:00
}
}
2023-03-17 20:55:47 +00:00
var advancedSection: some View {
2022-04-12 13:09:14 +00:00
Section {
let caption = L10n.Endpoint.Advanced.title
NavigationLink(caption) {
EndpointAdvancedView.OpenVPNView(
builder: $builder,
isReadonly: isConfigurationReadonly,
isServerPushed: false
).navigationTitle(caption)
}
}
}
2023-03-17 20:55:47 +00:00
func button(forEndpoint endpoint: Endpoint?) -> some View {
2022-04-12 13:09:14 +00:00
Button {
customEndpoint = endpoint
presentationMode.wrappedValue.dismiss()
} label: {
text(forEndpoint: endpoint)
}.withTrailingCheckmark(when: customEndpoint == endpoint)
}
func text(forEndpoint endpoint: Endpoint?) -> some View {
2022-04-12 13:09:14 +00:00
Text(endpoint?.address ?? L10n.Global.Strings.automatic)
.themeLongTextStyle()
2022-04-12 13:09:14 +00:00
}
var availableSocketTypes: [SocketType] {
guard let remotes = builder.remotes else {
return []
}
let allTypes: [SocketType] = [
SocketType.udp,
SocketType.tcp,
SocketType.udp4,
SocketType.tcp4
]
var availableTypes: [SocketType] = []
allTypes.forEach { socketType in
guard remotes.contains(where: {
$0.proto.socketType == socketType
}) else {
return
}
availableTypes.append(socketType)
}
return availableTypes
}
func allPorts(forSocketType socketType: SocketType) -> [UInt16] {
guard let remotes = builder.remotes else {
return []
}
let allPorts = Set(remotes.filter {
$0.proto.socketType == socketType
}.map(\.proto.port))
return Array(allPorts).sorted()
}
var filteredRemotes: [Endpoint]? {
builder.remotes?.filter {
$0.proto.socketType == selectedSocketType && $0.proto.port == selectedPort
}
}
var isConfigurationReadonly: Bool {
currentProfile.value.isProvider
}
2022-04-12 13:09:14 +00:00
}
// MARK: -
private extension EndpointView.OpenVPNView {
func onToggleAutomatic(_ value: Bool) {
2022-04-12 13:09:14 +00:00
if value {
guard customEndpoint != nil else {
return
}
customEndpoint = nil
}
}
func preselectFilters(once: Bool) {
2022-04-12 13:09:14 +00:00
guard !once || isFirstAppearance else {
return
}
isFirstAppearance = false
if let customEndpoint = customEndpoint {
isAutomatic = false
selectedSocketType = customEndpoint.proto.socketType
selectedPort = customEndpoint.proto.port
} else {
isAutomatic = true
guard let socketType = availableSocketTypes.first else {
2022-04-12 13:09:14 +00:00
assertionFailure("No socket types, empty remotes?")
return
}
selectedSocketType = socketType
preselectPort(forSocketType: socketType)
}
}
func preselectPort(forSocketType socketType: SocketType) {
2022-04-12 13:09:14 +00:00
let supported = allPorts(forSocketType: socketType)
guard !supported.contains(selectedPort) else {
return
}
guard let port = supported.first else {
assertionFailure("No ports, empty remotes?")
return
}
selectedPort = port
}
func scrollToCustomEndpoint(_ proxy: ScrollViewProxy) {
2022-04-12 13:09:14 +00:00
proxy.maybeScrollTo(customEndpoint?.id)
}
}
// MARK: - Bindings
private extension ObservableProfile {
func builderBinding(providerManager: ProviderManager) -> Binding<OpenVPN.ConfigurationBuilder> {
.init {
if self.value.isProvider {
guard let server = self.value.providerServer(providerManager) else {
assertionFailure("Server not found")
return .init()
}
guard let preset = self.value.providerPreset(server) else {
assertionFailure("Preset not found")
return .init()
}
guard let cfg = preset.openVPNConfiguration else {
assertionFailure("Preset \(preset.id) (\(preset.name)) has no OpenVPN configuration")
return .init()
}
var builder = cfg.builder(withFallbacks: true)
try? builder.setRemotes(from: preset, with: server, excludingHostname: false)
return builder
} else if let cfg = self.value.hostOpenVPNSettings?.configuration {
let builder = cfg.builder(withFallbacks: true)
// pp_log.debug("Loading OpenVPN configuration: \(builder)")
return builder
}
// fall back gracefully
return .init()
} set: {
if self.value.isProvider {
// readonly
} else {
pp_log.debug("Saving OpenVPN configuration: \($0)")
self.value.hostOpenVPNSettings?.configuration = $0.build()
}
}
}
var customEndpointBinding: Binding<Endpoint?> {
.init {
if self.value.isProvider {
return self.value.providerCustomEndpoint
} else {
return self.value.hostOpenVPNSettings?.customEndpoint
}
} set: {
if self.value.isProvider {
self.value.providerCustomEndpoint = $0
} else {
self.value.hostOpenVPNSettings?.customEndpoint = $0
}
}
}
}