//
// 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 CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI
extension OpenVPNView {
struct ConfigurationView: View {
let isServerPushed: Bool
let configuration: OpenVPN.Configuration.Builder
let credentialsRoute: (any Hashable)?
@ObservedObject
var excludedEndpoints: ObservableList
var body: some View {
moduleSection(for: accountRows, header: Strings.Global.Nouns.account)
remotesSection
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.Nouns.other)
}
}
}
// MARK: - Editable
private extension OpenVPNView.ConfigurationView {
var remotesSection: some View {
configuration.remotes.map { remotes in
ForEach(remotes, id: \.rawValue) { remote in
SelectableRemoteButton(
remote: remote,
all: Set(remotes),
excludedEndpoints: excludedEndpoints
)
}
.themeSection(header: Strings.Modules.Openvpn.remotes)
}
}
}
private struct SelectableRemoteButton: View {
let remote: ExtendedEndpoint
let all: Set
@ObservedObject
var excludedEndpoints: ObservableList
var body: some View {
Button {
if excludedEndpoints.contains(remote) {
excludedEndpoints.remove(remote)
} else {
if remaining.count > 1 {
excludedEndpoints.add(remote)
}
}
} label: {
HStack {
VStack(alignment: .leading) {
Text(remote.address.rawValue)
.font(.headline)
Text("\(remote.proto.socketType.rawValue):\(remote.proto.port.description)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
ThemeImage(.marked)
.opaque(!excludedEndpoints.contains(remote))
}
.contentShape(.rect)
}
.buttonStyle(.plain)
}
private var remaining: Set {
all.filter {
!excludedEndpoints.contains($0)
}
}
}
// MARK: - Constant
private extension OpenVPNView.ConfigurationView {
var accountRows: [ModuleRow]? {
guard let credentialsRoute else {
return nil
}
guard configuration.authUserPass == true else {
return nil
}
return [.push(
caption: Strings.Modules.Openvpn.credentials,
route: HashableRoute(credentialsRoute))
]
}
var pullRows: [ModuleRow]? {
configuration.pullMask?
.map(\.localizedDescription)
.sorted()
.map {
.text(caption: $0, 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.Nouns.address, value: $0))
}
ip.localizedDescription(optionalStyle: .defaultGateway).map {
rows.append(.copiableText(caption: Strings.Global.Nouns.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.Nouns.route, value: $0.localizedDescription))
}
return rows.nilIfEmpty
}
var redirectRows: [ModuleRow]? {
configuration.routingPolicies?
.compactMap {
switch $0 {
case .IPv4: return Strings.Unlocalized.ipv4
case .IPv6: return Strings.Unlocalized.ipv6
default: return nil
}
}
.sorted()
.map {
.text(caption: $0)
}
.nilIfEmpty
}
var dnsRows: [ModuleRow]? {
var rows: [ModuleRow] = []
configuration.dnsServers?
.nilIfEmpty
.map {
rows.append(.textList(
caption: Strings.Global.Nouns.servers,
values: $0
))
}
configuration.dnsDomain.map {
rows.append(.copiableText(
caption: Strings.Global.Nouns.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.Nouns.certificate, value: $0.pem, preview: nil))
}
configuration.clientKey.map {
rows.append(.longContentPreview(caption: Strings.Global.Nouns.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.Nouns.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
#Preview {
struct Preview: View {
@StateObject
private var excludedEndpoints = ObservableList { _ in
true
} add: { _ in
//
} remove: { _ in
//
}
var body: some View {
Form {
OpenVPNView.ConfigurationView(
isServerPushed: false,
configuration: .forPreviews,
credentialsRoute: nil,
excludedEndpoints: excludedEndpoints
)
}
.withMockEnvironment()
}
}
return Preview()
}