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

437 lines
15 KiB
Swift

//
// EndpointAdvancedView+OpenVPN.swift
// Passepartout
//
// Created by Davide De Rosa on 3/8/22.
// Copyright (c) 2023 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 PassepartoutLibrary
import SwiftUI
import TunnelKitOpenVPN
extension EndpointAdvancedView {
struct OpenVPNView: View {
@Binding var builder: OpenVPN.ConfigurationBuilder
let isReadonly: Bool
let isServerPushed: Bool
private let fallbackConfiguration = OpenVPN.ConfigurationBuilder(withFallbacks: true).build()
var body: some View {
List {
let cfg = builder.build()
if !isServerPushed {
pullSection(configuration: cfg)
}
if builder.ipv4 != nil || builder.routes4 != nil {
ipv4Section
}
if builder.ipv6 != nil || builder.routes6 != nil {
ipv6Section
}
dnsSection(configuration: cfg)
proxySection(configuration: cfg)
if !isReadonly {
communicationEditableSection
compressionEditableSection
} else {
communicationSection(configuration: cfg)
compressionSection(configuration: cfg)
}
if !isServerPushed {
tlsSection
}
otherSection(configuration: cfg)
}
}
}
}
// MARK: -
private extension EndpointAdvancedView.OpenVPNView {
func pullSection(configuration: OpenVPN.Configuration) -> some View {
configuration.pullMask.map { mask in
Section {
ForEach(mask, id: \.self) {
Text($0.localizedDescription)
}
} header: {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Pull.header)
}
}
}
var ipv4Section: some View {
Section {
if let settings = builder.ipv4 {
themeLongContentLinkDefault(
L10n.Global.Strings.address,
content: .constant(settings.localizedDescription(style: .address))
)
themeLongContentLinkDefault(
L10n.NetworkSettings.Gateway.title,
content: .constant(settings.localizedDescription(style: .defaultGateway))
)
}
builder.routes4.map { routes in
ForEach(routes, id: \.self) { route in
themeLongContentLinkDefault(
L10n.Endpoint.Advanced.Openvpn.Items.Route.caption,
content: .constant(route.localizedDescription)
)
}
}
} header: {
Text(Unlocalized.Network.ipv4)
}
}
var ipv6Section: some View {
Section {
if let settings = builder.ipv6 {
themeLongContentLinkDefault(
L10n.Global.Strings.address,
content: .constant(settings.localizedDescription(style: .address))
)
themeLongContentLinkDefault(
L10n.NetworkSettings.Gateway.title,
content: .constant(settings.localizedDescription(style: .defaultGateway))
)
}
builder.routes6.map { routes in
ForEach(routes, id: \.self) { route in
themeLongContentLinkDefault(
L10n.Endpoint.Advanced.Openvpn.Items.Route.caption,
content: .constant(route.localizedDescription)
)
}
}
} header: {
Text(Unlocalized.Network.ipv6)
}
}
func communicationSection(configuration: OpenVPN.Configuration) -> some View {
configuration.communicationSettings.map { settings in
Section {
settings.cipher.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.Cipher.caption)
.withTrailingText($0)
}
settings.digest.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption)
.withTrailingText($0)
}
if let xor = settings.xor {
themeLongContentLink(
Unlocalized.VPN.xor,
content: .constant(xor.longDescription),
withPreview: xor.shortDescription
)
} else {
Text(Unlocalized.VPN.xor)
.withTrailingText(L10n.Global.Strings.disabled)
}
} header: {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Communication.header)
}
}
}
var communicationEditableSection: some View {
Section {
themeTextPicker(
L10n.Endpoint.Advanced.Openvpn.Items.Cipher.caption,
selection: $builder.cipher ?? fallbackCipher,
values: OpenVPN.Cipher.available,
description: \.localizedDescription
)
themeTextPicker(
L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption,
selection: $builder.digest ?? fallbackDigest,
values: OpenVPN.Digest.available,
description: \.localizedDescription
)
if let xor = builder.xorMethod {
themeLongContentLink(
Unlocalized.VPN.xor,
content: .constant(xor.localizedDescription(style: .long)),
withPreview: xor.localizedDescription(style: .short)
)
} else {
Text(Unlocalized.VPN.xor)
.withTrailingText(L10n.Global.Strings.disabled)
}
} header: {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Communication.header)
}
}
func compressionSection(configuration: OpenVPN.Configuration) -> some View {
configuration.compressionSettings.map { settings in
Section {
settings.framing.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionFraming.caption)
.withTrailingText($0)
}
settings.algorithm.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionAlgorithm.caption)
.withTrailingText($0)
}
} header: {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Compression.header)
}
}
}
var compressionEditableSection: some View {
Section {
themeTextPicker(
L10n.Endpoint.Advanced.Openvpn.Items.CompressionFraming.caption,
selection: $builder.compressionFraming ?? fallbackCompressionFraming,
values: OpenVPN.CompressionFraming.available,
description: \.localizedDescription
)
themeTextPicker(
L10n.Endpoint.Advanced.Openvpn.Items.CompressionAlgorithm.caption,
selection: $builder.compressionAlgorithm ?? fallbackCompressionAlgorithm,
values: OpenVPN.CompressionAlgorithm.available,
description: \.localizedDescription
).disabled(builder.compressionFraming == .disabled)
} header: {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Compression.header)
}
}
func dnsSection(configuration: OpenVPN.Configuration) -> some View {
configuration.dnsSettings.map { settings in
Section {
ForEach(settings.servers, id: \.self) {
Text(L10n.Global.Strings.address)
.withTrailingText($0, copyOnTap: true)
}
ForEach(settings.domains, id: \.self) {
Text(L10n.Global.Strings.domain)
.withTrailingText($0, copyOnTap: true)
}
} header: {
Text(Unlocalized.Network.dns)
}
}
}
func proxySection(configuration: OpenVPN.Configuration) -> some View {
configuration.proxySettings.map { settings in
Section {
settings.proxy.map {
Text(L10n.Global.Strings.address)
.withTrailingText($0, copyOnTap: true)
}
settings.pac.map {
Text(Unlocalized.Network.proxyAutoConfiguration)
.withTrailingText($0, copyOnTap: true)
}
ForEach(settings.bypass, id: \.self) {
Text(L10n.NetworkSettings.Items.ProxyBypass.caption)
.withTrailingText($0, copyOnTap: true)
}
} header: {
Text(L10n.Global.Strings.proxy)
}
}
}
var tlsSection: some View {
Section {
builder.ca.map { ca in
themeLongContentLink(
Unlocalized.VPN.certificateAuthority,
content: .constant(ca.pem)
)
}
builder.clientCertificate.map { cert in
themeLongContentLink(
L10n.Endpoint.Advanced.Openvpn.Items.Client.caption,
content: .constant(cert.pem)
)
}
builder.clientKey.map { key in
themeLongContentLink(
L10n.Endpoint.Advanced.Openvpn.Items.ClientKey.caption,
content: .constant(key.pem)
)
}
builder.tlsWrap.map { wrap in
themeLongContentLink(
L10n.Endpoint.Advanced.Openvpn.Items.TlsWrapping.caption,
content: .constant(wrap.key.hexString),
withPreview: builder.localizedDescription(style: .tlsWrap)
)
}
Text(L10n.Endpoint.Advanced.Openvpn.Items.Eku.caption)
.withTrailingText(builder.localizedDescription(style: .eku))
} header: {
Text(Unlocalized.Network.tls)
}
}
func otherSection(configuration: OpenVPN.Configuration) -> some View {
configuration.otherSettings.map { settings in
Section {
settings.keepAlive.map {
Text(L10n.Global.Strings.keepalive)
.withTrailingText($0)
}
settings.reneg.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.RenegotiationSeconds.caption)
.withTrailingText($0)
}
settings.randomizeEndpoint.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomEndpoint.caption)
.withTrailingText($0)
}
settings.randomizeHostnames.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomHostname.caption)
.withTrailingText($0)
}
} header: {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Other.header)
}
}
}
}
private extension OpenVPN.Configuration {
struct CommunicationOptions {
let cipher: String?
let digest: String?
let xor: (shortDescription: String, longDescription: String)?
}
struct CompressionOptions {
let framing: String?
let algorithm: String?
}
struct DNSOptions {
let servers: [String]
let domains: [String]
}
struct ProxyOptions {
let proxy: String?
let pac: String?
let bypass: [String]
}
struct OtherOptions {
let keepAlive: String?
let reneg: String?
let randomizeEndpoint: String?
let randomizeHostnames: String?
}
var communicationSettings: CommunicationOptions? {
guard cipher != nil || digest != nil || xorMethod != nil else {
return nil
}
return .init(
cipher: cipher?.localizedDescription,
digest: digest?.localizedDescription,
xor: xorMethod.map {
($0.localizedDescription(style: .short), $0.localizedDescription(style: .long))
}
)
}
var compressionSettings: CompressionOptions? {
guard compressionFraming != nil || compressionAlgorithm != nil else {
return nil
}
return .init(
framing: compressionFraming?.localizedDescription,
algorithm: compressionAlgorithm?.localizedDescription
)
}
var dnsSettings: DNSOptions? {
guard !(dnsServers?.isEmpty ?? true) || !(searchDomains?.isEmpty ?? true) else {
return nil
}
return .init(servers: dnsServers ?? [], domains: searchDomains ?? [])
}
var proxySettings: ProxyOptions? {
guard httpsProxy != nil || httpProxy != nil ||
proxyAutoConfigurationURL != nil || !(proxyBypassDomains?.isEmpty ?? true) else {
return nil
}
return .init(
proxy: (httpsProxy ?? httpProxy)?.rawValue,
pac: proxyAutoConfigurationURL?.absoluteString,
bypass: proxyBypassDomains ?? []
)
}
var otherSettings: OtherOptions? {
guard keepAliveInterval != nil || renegotiatesAfter != nil ||
randomizeEndpoint != nil || randomizeHostnames != nil else {
return nil
}
return .init(
keepAlive: localizedDescription(optionalStyle: .keepAlive),
reneg: localizedDescription(optionalStyle: .renegotiatesAfter),
randomizeEndpoint: localizedDescription(optionalStyle: .randomizeEndpoint),
randomizeHostnames: localizedDescription(optionalStyle: .randomizeHostnames)
)
}
}
private extension EndpointAdvancedView.OpenVPNView {
var fallbackCipher: OpenVPN.Cipher {
fallbackConfiguration.cipher!
}
var fallbackDigest: OpenVPN.Digest {
fallbackConfiguration.digest!
}
var fallbackCompressionFraming: OpenVPN.CompressionFraming {
fallbackConfiguration.compressionFraming!
}
var fallbackCompressionAlgorithm: OpenVPN.CompressionAlgorithm {
fallbackConfiguration.compressionAlgorithm!
}
}