Skip to content

Commit

Permalink
Import VPN configuration file into module (#1238)
Browse files Browse the repository at this point in the history
Add feature to WireGuard, and unify behavior in OpenVPN. Always allow
import, even if a configuration exists in the module.

The ImportModifier might be reused later if more modules need it (there
is some degree of duplication).
  • Loading branch information
keeshux authored Mar 7, 2025
1 parent 1f36520 commit 7540eb0
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import CommonLibrary
import CommonUtils
import PassepartoutKit
import SwiftUI
import UILibrary

extension OpenVPNModule.Builder: ModuleViewProviding {
public func moduleView(with parameters: ModuleViewParameters) -> some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,30 @@ private extension OpenVPNView {
var contentView: some View {
if draft.module.configurationBuilder != nil {
if !isServerPushed {
remotesLink
ModuleImportSection(isImporting: $isImporting)
connectionSection
}
ConfigurationView(
isServerPushed: isServerPushed,
configuration: $draft.module.configurationBuilder ?? .init(),
credentialsRoute: ProfileRoute(OpenVPNModule.Subroute.credentials)
)
} else if draft.module.providerSelection != nil,
let providerConfiguration {
Section {
remotesLink
providerConfigurationLink(with: providerConfiguration)
}
.modifier(providerModifier)
} else {
emptyConfigurationView
ModuleImportSection(isImporting: $isImporting)
.modifier(providerModifier)
}
}

@ViewBuilder
var emptyConfigurationView: some View {
if draft.module.providerSelection == nil {
importButton
} else if let providerConfiguration {
remotesLink
providerConfigurationLink(with: providerConfiguration)
}
var connectionSection: some View {
remotesLink
.themeSection(header: Strings.Global.Nouns.connection)
}

var remotesLink: some View {
Expand All @@ -121,12 +124,6 @@ private extension OpenVPNView {
NavigationLink(Strings.Global.Nouns.configuration, value: ProfileRoute(OpenVPNModule.Subroute.providerConfiguration(configuration)))
}

var importButton: some View {
Button(Strings.Modules.General.Rows.importFromFile.forMenu) {
isImporting = true
}
}

var providerModifier: some ViewModifier {
ProviderContentModifier(
providerId: providerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//

import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//

import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//

import CommonLibrary
import PassepartoutKit
import SwiftUI
import UILibrary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//

import CommonUtils
import PassepartoutKit
import SwiftUI
import UILibrary

extension WireGuardModule.Builder: ModuleViewProviding {
public func moduleView(with parameters: ModuleViewParameters) -> some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ extension WireGuardView {
.onLoad {
model.load(from: configuration)
}
.onChange(of: configuration) {
model.load(from: $0)
}
.onChange(of: model) {
$0.save(to: &configuration)
}
Expand All @@ -65,9 +68,9 @@ private extension WireGuardView.ConfigurationView {
value: $model.privateKey
)
if let keyGenerator {
ThemeModuleLongContent(
ThemeModuleCopiableText(
caption: Strings.Global.Nouns.publicKey,
value: .constant((try? keyGenerator.publicKey(for: model.privateKey)) ?? "")
value: (try? keyGenerator.publicKey(for: model.privateKey)) ?? ""
)
Button(Strings.Modules.Wireguard.PrivateKey.generate) {
model.privateKey = keyGenerator.newPrivateKey()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// WireGuardView+Import.swift
// Passepartout
//
// Created by Davide De Rosa on 3/7/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 CommonUtils
import PassepartoutKit
import SwiftUI

extension WireGuardView {
struct ImportModifier: ViewModifier {

@ObservedObject
var draft: ModuleDraft<WireGuardModule.Builder>

let impl: WireGuardModule.Implementation?

@Binding
var isImporting: Bool

@ObservedObject
var errorHandler: ErrorHandler

@State
private var importURL: URL?

func body(content: Content) -> some View {
content
.fileImporter(
isPresented: $isImporting,
allowedContentTypes: [.item],
onCompletion: importConfiguration
)
}
}
}

private extension WireGuardView.ImportModifier {
func importConfiguration(from result: Result<URL, Error>) {
do {
let url = try result.get()
guard url.startAccessingSecurityScopedResource() else {
throw AppError.permissionDenied
}
defer {
url.stopAccessingSecurityScopedResource()
}
importURL = url

guard let impl else {
fatalError("Requires WireGuardModule implementation")
}
let parsed: Module
do {
parsed = try impl.importer.module(fromURL: url, object: nil)
} catch let error as PassepartoutError {
pp_log(.app, .error, "Unable to parse URL: \(error)")

switch error.code {
case .unknownImportedModule:
throw PassepartoutError(.parsing)

default:
throw error
}
}
guard let module = parsed as? WireGuardModule else {
throw PassepartoutError(.parsing)
}
draft.module.configurationBuilder = module.configuration?.builder()
} catch {
pp_log(.app, .error, "Unable to import WireGuard configuration: \(error)")
errorHandler.handle(error, title: draft.module.moduleType.localizedDescription)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ struct WireGuardView: View, ModuleDraftEditing {
@State
private var paywallReason: PaywallReason?

@State
private var isImporting = false

@StateObject
private var errorHandler: ErrorHandler = .default()

Expand All @@ -52,6 +55,12 @@ struct WireGuardView: View, ModuleDraftEditing {
var body: some View {
contentView
.moduleView(draft: draft)
.modifier(ImportModifier(
draft: draft,
impl: impl,
isImporting: $isImporting,
errorHandler: errorHandler
))
.themeAnimation(on: draft.module.providerId, category: .modules)
.modifier(PaywallModifier(reason: $paywallReason))
.withErrorHandler(errorHandler)
Expand All @@ -65,14 +74,15 @@ private extension WireGuardView {
@ViewBuilder
var contentView: some View {
if draft.module.configurationBuilder != nil {
ModuleImportSection(isImporting: $isImporting)
ConfigurationView(
configuration: $draft.module.configurationBuilder ?? impl.map {
.init(keyGenerator: $0.keyGenerator)
} ?? .init(privateKey: ""),
keyGenerator: impl?.keyGenerator
)
} else {
EmptyView()
ModuleImportSection(isImporting: $isImporting)
.modifier(providerModifier)
}
}
Expand Down Expand Up @@ -107,10 +117,6 @@ private extension WireGuardView {
path.wrappedValue.removeLast()
}

func importConfiguration(from url: URL) {
// TODO: #397, import draft from external URL
}

func editConfiguration() {
// TODO: #397, edit configuration as text
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public struct ThemeModuleCopiableText: View {
public var body: some View {
ThemeCopiableText(title: caption, value: value, isMultiLine: multiline) {
Text($0)
.themeTruncating(.middle)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// ModuleImportSection.swift
// Passepartout
//
// Created by Davide De Rosa on 3/7/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 SwiftUI

public struct ModuleImportSection: View {

@Binding
private var isImporting: Bool

public init(isImporting: Binding<Bool>) {
_isImporting = isImporting
}

public var body: some View {
Section {
Button(Strings.Modules.General.Rows.importFromFile.forMenu) {
isImporting = true
}
}
}
}

0 comments on commit 7540eb0

Please sign in to comment.