diff --git a/Passepartout-iOS/AppDelegate.swift b/Passepartout-iOS/AppDelegate.swift index 81c4e41f..f77b31a8 100644 --- a/Passepartout-iOS/AppDelegate.swift +++ b/Passepartout-iOS/AppDelegate.swift @@ -32,6 +32,8 @@ import Convenience class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { var window: UIWindow? + + private var importer: HostImporter? override init() { AppConstants.Log.configure() @@ -101,26 +103,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele } private func tryParseURL(_ url: URL, passphrase: String?, target: UIViewController) -> Bool { - let passphraseBlock = { (passphrase) in - _ = self.tryParseURL(url, passphrase: passphrase, target: target) + guard let rootViewController = window?.rootViewController else { + return false } - let passphraseCancelBlock = { - _ = try? FileManager.default.removeItem(at: url) + importer = HostImporter(withConfigurationURL: url, parentViewController: rootViewController) + importer?.importHost(withPassphrase: passphrase, removeOnError: true, removeOnCancel: true) { + self.handleParsingResult($0, in: rootViewController) } - guard let parsingResult = OpenVPN.ConfigurationParser.Result.from(url, withErrorAlertIn: target, passphrase: passphrase, removeOnError: true, passphraseBlock: passphraseBlock, passphraseCancelBlock: passphraseCancelBlock) else { - return true - } - if let warning = parsingResult.warning { - OpenVPN.ConfigurationParser.Result.alertImportWarning(url: url, in: target, withWarning: warning) { - if $0 { - self.handleParsingResult(parsingResult, in: target) - } else { - try? FileManager.default.removeItem(at: url) - } - } - return true - } - handleParsingResult(parsingResult, in: target) return true } diff --git a/Passepartout-iOS/Global/ConfigurationParserResult+Alerts.swift b/Passepartout-iOS/Global/ConfigurationParserResult+Alerts.swift deleted file mode 100644 index 157711d3..00000000 --- a/Passepartout-iOS/Global/ConfigurationParserResult+Alerts.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// OpenVPN.ConfigurationParserResult+Alerts.swift -// Passepartout-iOS -// -// Created by Davide De Rosa on 10/27/18. -// Copyright (c) 2019 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 Foundation -import UIKit -import TunnelKit -import SwiftyBeaver -import PassepartoutCore - -private let log = SwiftyBeaver.self - -extension OpenVPN.ConfigurationParser.Result { - static func from(_ url: URL, withErrorAlertIn viewController: UIViewController, passphrase: String?, removeOnError: Bool, - passphraseBlock: @escaping (String) -> Void, passphraseCancelBlock: (() -> Void)?) -> OpenVPN.ConfigurationParser.Result? { - - let result: OpenVPN.ConfigurationParser.Result - let fm = FileManager.default - - log.debug("Parsing configuration URL: \(url)") - do { - result = try OpenVPN.ConfigurationParser.parsed(fromURL: url, passphrase: passphrase) - } catch let e as ConfigurationError { - switch e { - case .encryptionPassphrase, .unableToDecrypt(_): - let alert = UIAlertController.asAlert(url.normalizedFilename, L10n.Core.ParsedFile.Alerts.EncryptionPassphrase.message) - alert.addTextField { (field) in - field.isSecureTextEntry = true - } - alert.addPreferredAction(L10n.Core.Global.ok) { - guard let passphrase = alert.textFields?.first?.text else { - return - } - passphraseBlock(passphrase) - } - alert.addCancelAction(L10n.Core.Global.cancel) { - passphraseCancelBlock?() - } - viewController.present(alert, animated: true, completion: nil) - - default: - let message = localizedMessage(forError: e) - alertImportError(url: url, in: viewController, withMessage: message) - if removeOnError { - try? fm.removeItem(at: url) - } - } - return nil - } catch let e { - let message = localizedMessage(forError: e) - alertImportError(url: url, in: viewController, withMessage: message) - if removeOnError { - try? fm.removeItem(at: url) - } - return nil - } - return result - } - - private static func alertImportError(url: URL, in vc: UIViewController, withMessage message: String) { - let alert = UIAlertController.asAlert(url.normalizedFilename, message) -// alert.addPreferredAction(L10n.Core.ParsedFile.Alerts.Buttons.report) { -// var attach = IssueReporter.Attachments(debugLog: false, configurationURL: url) -// attach.description = message -// IssueReporter.shared.present(in: vc, withAttachments: attach) -// } - alert.addCancelAction(L10n.Core.Global.ok) - vc.present(alert, animated: true, completion: nil) - } - - static func alertImportWarning(url: URL, in vc: UIViewController, withWarning warning: ConfigurationError, completionHandler: @escaping (Bool) -> Void) { - let message = details(forWarning: warning) - let alert = UIAlertController.asAlert(url.normalizedFilename, L10n.Core.ParsedFile.Alerts.PotentiallyUnsupported.message(message)) - alert.addPreferredAction(L10n.Core.Global.ok) { - completionHandler(true) - } - alert.addCancelAction(L10n.Core.Global.cancel) { - completionHandler(false) - } - vc.present(alert, animated: true, completion: nil) - } - - private static func localizedMessage(forError error: Error) -> String { - if let appError = error as? ConfigurationError { - switch appError { - case .malformed(let option): - log.error("Could not parse configuration URL: malformed option, \(option)") - return L10n.Core.ParsedFile.Alerts.Malformed.message(option) - - case .missingConfiguration(let option): - log.error("Could not parse configuration URL: missing configuration, \(option)") - return L10n.Core.ParsedFile.Alerts.Missing.message(option) - - case .unsupportedConfiguration(var option): - if option.contains("external") { - option.append(" - see FAQ") - } - log.error("Could not parse configuration URL: unsupported configuration, \(option)") - return L10n.Core.ParsedFile.Alerts.Unsupported.message(option) - - default: - break - } - } - log.error("Could not parse configuration URL: \(error)") - return L10n.Core.ParsedFile.Alerts.Parsing.message(error.localizedDescription) - } - - private static func details(forWarning warning: ConfigurationError) -> String { - switch warning { - case .malformed(let option): - return option - - case .missingConfiguration(let option): - return option - - case .unsupportedConfiguration(var option): - if option.contains("external") { - option.append(" - see FAQ") - } - return option - - default: - return "" // XXX: should never get here - } - } -} diff --git a/Passepartout-iOS/Global/HostImporter.swift b/Passepartout-iOS/Global/HostImporter.swift new file mode 100644 index 00000000..59a9159c --- /dev/null +++ b/Passepartout-iOS/Global/HostImporter.swift @@ -0,0 +1,170 @@ +// +// HostImporter.swift +// Passepartout-macOS +// +// Created by Davide De Rosa on 10/22/19. +// Copyright (c) 2019 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 Foundation +import PassepartoutCore +import TunnelKit +import SwiftyBeaver + +private let log = SwiftyBeaver.self + +class HostImporter { + private let service = TransientStore.shared.service + + private weak var viewController: UIViewController? + + private let configurationURL: URL + + init(withConfigurationURL configurationURL: URL, parentViewController: UIViewController) { + self.configurationURL = configurationURL + log.debug("Parsing configuration URL: \(configurationURL)") + + viewController = parentViewController + } + + func importHost(withPassphrase passphrase: String?, removeOnError: Bool, removeOnCancel: Bool, completionHandler: @escaping (OpenVPN.ConfigurationParser.Result) -> Void) { + let result: OpenVPN.ConfigurationParser.Result + do { + result = try OpenVPN.ConfigurationParser.parsed(fromURL: configurationURL, passphrase: passphrase) + } catch let e as ConfigurationError { + switch e { + case .encryptionPassphrase, .unableToDecrypt(_): + enterPassphraseForHost(at: configurationURL, removeOnError: removeOnError, removeOnCancel: removeOnCancel, completionHandler: completionHandler) + + default: + alertImportError(e, removeOnError: removeOnError) + } + return + } catch let e { + alertImportError(e, removeOnError: removeOnError) + return + } + + if let warning = result.warning { + alertImportWarning(warning, removeOnCancel: removeOnCancel) { + completionHandler(result) + } + return + } + + completionHandler(result) + } + + private func alertImportError(_ error: Error, removeOnError: Bool) { + let message = HostImporter.localizedMessage(forError: error) + let alert = UIAlertController.asAlert(configurationURL.normalizedFilename, message) + alert.addCancelAction(L10n.Core.Global.ok) + viewController?.present(alert, animated: true, completion: nil) + + if removeOnError { + try? FileManager.default.removeItem(at: configurationURL) + } + } + + private func alertImportWarning(_ warning: ConfigurationError, removeOnCancel: Bool, completionHandler: @escaping () -> Void) { + let message = HostImporter.localizedDetailsMessage(forWarning: warning) + let alert = UIAlertController.asAlert(configurationURL.normalizedFilename, L10n.Core.ParsedFile.Alerts.PotentiallyUnsupported.message(message)) + alert.addPreferredAction(L10n.Core.Global.ok) { + completionHandler() + } + alert.addCancelAction(L10n.Core.Global.cancel) { + if removeOnCancel { + try? FileManager.default.removeItem(at: self.configurationURL) + } + } + viewController?.present(alert, animated: true, completion: nil) + } + + private func enterPassphraseForHost(at url: URL, removeOnError: Bool, removeOnCancel: Bool, completionHandler: @escaping (OpenVPN.ConfigurationParser.Result) -> Void) { + let alert = UIAlertController.asAlert(configurationURL.normalizedFilename, L10n.Core.ParsedFile.Alerts.EncryptionPassphrase.message) + alert.addTextField { (field) in + field.isSecureTextEntry = true + } + alert.addPreferredAction(L10n.Core.Global.ok) { + guard let passphrase = alert.textFields?.first?.text else { + return + } + self.importHost( + withPassphrase: passphrase, + removeOnError: removeOnError, + removeOnCancel: removeOnCancel, + completionHandler: completionHandler + ) + } + alert.addCancelAction(L10n.Core.Global.cancel) { + if removeOnCancel { + try? FileManager.default.removeItem(at: url) + } + } + viewController?.present(alert, animated: true, completion: nil) + } + + // MARK: Helpers + + private static func localizedMessage(forError error: Error) -> String { + if let appError = error as? ConfigurationError { + switch appError { + case .malformed(let option): + log.error("Could not parse configuration URL: malformed option, \(option)") + return L10n.Core.ParsedFile.Alerts.Malformed.message(option) + + case .missingConfiguration(let option): + log.error("Could not parse configuration URL: missing configuration, \(option)") + return L10n.Core.ParsedFile.Alerts.Missing.message(option) + + case .unsupportedConfiguration(var option): + if option.contains("external") { + option.append(" (see FAQ)") + } + log.error("Could not parse configuration URL: unsupported configuration, \(option)") + return L10n.Core.ParsedFile.Alerts.Unsupported.message(option) + + default: + break + } + } + log.error("Could not parse configuration URL: \(error)") + return L10n.Core.ParsedFile.Alerts.Parsing.message(error.localizedDescription) + } + + private static func localizedDetailsMessage(forWarning warning: ConfigurationError) -> String { + switch warning { + case .malformed(let option): + return option + + case .missingConfiguration(let option): + return option + + case .unsupportedConfiguration(var option): + if option.contains("external") { + option.append(" (see FAQ)") + } + return option + + default: + return "" // XXX: should never get here + } + } +} diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift index 1c0b3ac9..5210f0f1 100644 --- a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift @@ -32,6 +32,8 @@ private let log = SwiftyBeaver.self class ImportedHostsViewController: UITableViewController { private lazy var pendingConfigurationURLs = TransientStore.shared.service.pendingConfigurationURLs().sortedCaseInsensitive() + + private var importer: HostImporter? private var parsingResult: OpenVPN.ConfigurationParser.Result? @@ -76,35 +78,20 @@ class ImportedHostsViewController: UITableViewController { return false } let url = pendingConfigurationURLs[indexPath.row] - return tryParseURL(url, passphrase: nil, cell: cell) + return tryParseURL(url, cell: cell) } return true } - private func tryParseURL(_ url: URL, passphrase: String?, cell: UITableViewCell) -> Bool { - let passphraseBlock: (String) -> Void = { (passphrase) in - guard self.tryParseURL(url, passphrase: passphrase, cell: cell) else { - return - } - self.perform(segue: StoryboardSegue.Organizer.importHostSegueIdentifier, sender: cell) - } - guard let parsingResult = OpenVPN.ConfigurationParser.Result.from(url, withErrorAlertIn: self, passphrase: passphrase, removeOnError: false, passphraseBlock: passphraseBlock, passphraseCancelBlock: nil) else { - deselectSelectedRow() - return false - } - self.parsingResult = parsingResult - - // postpone segue until alert dismissal - if let warning = parsingResult.warning { - OpenVPN.ConfigurationParser.Result.alertImportWarning(url: url, in: self, withWarning: warning) { - self.deselectSelectedRow() - if $0 { - self.perform(segue: StoryboardSegue.Organizer.importHostSegueIdentifier) - } else { - self.parsingResult = nil - } - } - return false + private func tryParseURL(_ url: URL, cell: UITableViewCell) -> Bool { + importer = HostImporter(withConfigurationURL: url, parentViewController: self) + importer?.importHost(withPassphrase: nil, removeOnError: false, removeOnCancel: false) { + + // FIXME: HostImporter, also deselect on error/cancel, or just deselect immediately + self.deselectSelectedRow() + + self.parsingResult = $0 + self.perform(segue: StoryboardSegue.Organizer.importHostSegueIdentifier) } return true } diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index e52a9e4e..3d696a96 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ 0E3152DA223FA05800F61841 /* PlaceholderConnectionProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E79D13E21919EC900BB5FB2 /* PlaceholderConnectionProfile.swift */; }; 0E3152DB223FA05800F61841 /* ProfileKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E79D14021919F5600BB5FB2 /* ProfileKey.swift */; }; 0E3152DC223FA05800F61841 /* ProviderConnectionProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE3AA4213DC1B000BFA2F5 /* ProviderConnectionProfile.swift */; }; + 0E3262D9235EE8DA00B5E470 /* HostImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3262D8235EE8DA00B5E470 /* HostImporter.swift */; }; 0E3419AD2350815E00419E18 /* Donation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3419AC2350815E00419E18 /* Donation.swift */; }; 0E3586FE225BD34800509A4D /* ActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3586FD225BD34800509A4D /* ActivityTableViewCell.swift */; }; 0E36D24D2240234B006AF062 /* ShortcutsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E36D24C2240234B006AF062 /* ShortcutsAddViewController.swift */; }; @@ -83,7 +84,6 @@ 0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */; }; 0E9CD7872257462800D033B4 /* Providers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E9CD7862257462800D033B4 /* Providers.xcassets */; }; 0E9CD789225746B300D033B4 /* Flags.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E9CD788225746B300D033B4 /* Flags.xcassets */; }; - 0EA068F4218475F800C320AD /* ConfigurationParserResult+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA068F3218475F800C320AD /* ConfigurationParserResult+Alerts.swift */; }; 0EAAD71920E6669A0088754A /* GroupConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE8DED20C93E4C004C739C /* GroupConstants.swift */; }; 0EB60FDA2111136E00AD27F3 /* UITextView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */; }; 0EB67D6B2184581E00BA6200 /* ImportedHostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */; }; @@ -179,6 +179,7 @@ 0E31529B223F9EF400F61841 /* PassepartoutCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PassepartoutCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0E31529D223F9EF500F61841 /* PassepartoutCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PassepartoutCore.h; sourceTree = ""; }; 0E31529E223F9EF500F61841 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0E3262D8235EE8DA00B5E470 /* HostImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostImporter.swift; sourceTree = ""; }; 0E3419AC2350815E00419E18 /* Donation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Donation.swift; sourceTree = ""; }; 0E3586FD225BD34800509A4D /* ActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityTableViewCell.swift; sourceTree = ""; }; 0E36D24C2240234B006AF062 /* ShortcutsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsAddViewController.swift; sourceTree = ""; }; @@ -247,7 +248,6 @@ 0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfrastructurePreset.swift; sourceTree = ""; }; 0E9CD7862257462800D033B4 /* Providers.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Providers.xcassets; sourceTree = ""; }; 0E9CD788225746B300D033B4 /* Flags.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Flags.xcassets; sourceTree = ""; }; - 0EA068F3218475F800C320AD /* ConfigurationParserResult+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationParserResult+Alerts.swift"; sourceTree = ""; }; 0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Search.swift"; sourceTree = ""; }; 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedHostsViewController.swift; sourceTree = ""; }; 0EBBE8F42182361700106008 /* ConnectionService+Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConnectionService+Migration.swift"; sourceTree = ""; }; @@ -534,8 +534,8 @@ isa = PBXGroup; children = ( 0E45E6E222BD793800F19312 /* App.strings */, - 0EA068F3218475F800C320AD /* ConfigurationParserResult+Alerts.swift */, 0E3419AC2350815E00419E18 /* Donation.swift */, + 0E3262D8235EE8DA00B5E470 /* HostImporter.swift */, 0EFD943D215BE10800529B64 /* IssueReporter.swift */, 0E4FD7F020D58618002221FF /* Macros.swift */, 0E24273F225951B00064A1A3 /* ProductManager.swift */, @@ -986,6 +986,7 @@ 0E05C5D620D1645F006EE732 /* SwiftGen+Scenes.swift in Sources */, 0E773BF8224BF37600CDDC8E /* ShortcutsViewController.swift in Sources */, 0E3419AD2350815E00419E18 /* Donation.swift in Sources */, + 0E3262D9235EE8DA00B5E470 /* HostImporter.swift in Sources */, 0EFD9440215BED8E00529B64 /* LabelViewController.swift in Sources */, 0ED31C2C20CF2D6F0027975F /* ProviderPoolViewController.swift in Sources */, 0E2B494020FCFF990094784C /* Theme+Titles.swift in Sources */, @@ -1000,7 +1001,6 @@ 0EB67D6B2184581E00BA6200 /* ImportedHostsViewController.swift in Sources */, 0E57F63E20C83FC5008323CF /* ServiceViewController.swift in Sources */, 0E36D24D2240234B006AF062 /* ShortcutsAddViewController.swift in Sources */, - 0EA068F4218475F800C320AD /* ConfigurationParserResult+Alerts.swift in Sources */, 0E57F63C20C83FC5008323CF /* AppDelegate.swift in Sources */, 0ED31C2920CF2A340027975F /* AccountViewController.swift in Sources */, 0E158ADA20E11B0B00C85A82 /* EndpointViewController.swift in Sources */,