// // 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() }