Merge branch 'improve-configuration-code'
This commit is contained in:
commit
fa884f8d90
|
@ -24,9 +24,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftyBeaver
|
|
||||||
|
|
||||||
private let log = SwiftyBeaver.self
|
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
|
||||||
|
@ -87,60 +84,41 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: URLs
|
// MARK: URLs
|
||||||
|
|
||||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||||
guard let root = window?.rootViewController else {
|
guard let root = window?.rootViewController else {
|
||||||
fatalError("No window.rootViewController?")
|
fatalError("No window.rootViewController?")
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
guard let parsedFile = ParsedFile.from(url, withErrorAlertIn: root) else {
|
||||||
|
return true
|
||||||
// already presented: update URL
|
|
||||||
if let nav = root.presentedViewController as? UINavigationController, let wizard = nav.topViewController as? WizardHostViewController {
|
|
||||||
try wizard.setConfigurationURL(url)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// present now
|
|
||||||
let nav = StoryboardScene.Organizer.wizardHostIdentifier.instantiate()
|
|
||||||
guard let wizard = nav.topViewController as? WizardHostViewController else {
|
|
||||||
fatalError("Expected WizardHostViewController from storyboard")
|
|
||||||
}
|
|
||||||
try wizard.setConfigurationURL(url)
|
|
||||||
|
|
||||||
// best effort to delegate to main vc
|
|
||||||
let split = root as? UISplitViewController
|
|
||||||
let master = split?.viewControllers.first as? UINavigationController
|
|
||||||
master?.viewControllers.forEach {
|
|
||||||
if let organizerVC = $0 as? OrganizerViewController {
|
|
||||||
wizard.delegate = organizerVC
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nav.modalPresentationStyle = .formSheet
|
|
||||||
root.present(nav, animated: true, completion: nil)
|
|
||||||
} catch ApplicationError.missingConfiguration(let option) {
|
|
||||||
let message = L10n.Wizards.Host.Alerts.Missing.message(option)
|
|
||||||
alertConfigurationImportError(url: url, in: root, withMessage: message)
|
|
||||||
} catch ApplicationError.unsupportedConfiguration(let option) {
|
|
||||||
let message = L10n.Wizards.Host.Alerts.Unsupported.message(option)
|
|
||||||
alertConfigurationImportError(url: url, in: root, withMessage: message)
|
|
||||||
} catch let e {
|
|
||||||
let message = L10n.Wizards.Host.Alerts.Parsing.message(e.localizedDescription)
|
|
||||||
alertConfigurationImportError(url: url, in: root, withMessage: message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// already presented: update parsed configuration
|
||||||
|
if let nav = root.presentedViewController as? UINavigationController, let wizard = nav.topViewController as? WizardHostViewController {
|
||||||
|
wizard.parsedFile = parsedFile
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// present now
|
||||||
|
let nav = StoryboardScene.Organizer.wizardHostIdentifier.instantiate()
|
||||||
|
guard let wizard = nav.topViewController as? WizardHostViewController else {
|
||||||
|
fatalError("Expected WizardHostViewController from storyboard")
|
||||||
|
}
|
||||||
|
wizard.parsedFile = parsedFile
|
||||||
|
|
||||||
|
// best effort to delegate to main vc
|
||||||
|
let split = root as? UISplitViewController
|
||||||
|
let master = split?.viewControllers.first as? UINavigationController
|
||||||
|
master?.viewControllers.forEach {
|
||||||
|
if let organizerVC = $0 as? OrganizerViewController {
|
||||||
|
wizard.delegate = organizerVC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nav.modalPresentationStyle = .formSheet
|
||||||
|
root.present(nav, animated: true, completion: nil)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func alertConfigurationImportError(url: URL, in vc: UIViewController, withMessage message: String) {
|
|
||||||
let alert = Macros.alert(L10n.Organizer.Sections.Hosts.header, message)
|
|
||||||
// alert.addDefaultAction(L10n.Wizards.Host.Alerts.Buttons.report) {
|
|
||||||
// var attach = IssueReporter.Attachments(debugLog: false, configurationURL: url)
|
|
||||||
// attach.description = message
|
|
||||||
// IssueReporter.shared.present(in: vc, withAttachments: attach)
|
|
||||||
// }
|
|
||||||
alert.addCancelAction(L10n.Global.cancel)
|
|
||||||
vc.present(alert, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UISplitViewController {
|
extension UISplitViewController {
|
||||||
|
|
|
@ -90,10 +90,9 @@ class IssueReporter: NSObject {
|
||||||
vc.addAttachmentData(attachment, mimeType: AppConstants.IssueReporter.MIME.debugLog, fileName: AppConstants.IssueReporter.Filenames.debugLog)
|
vc.addAttachmentData(attachment, mimeType: AppConstants.IssueReporter.MIME.debugLog, fileName: AppConstants.IssueReporter.Filenames.debugLog)
|
||||||
}
|
}
|
||||||
if let url = configurationURL {
|
if let url = configurationURL {
|
||||||
var lines: [String] = []
|
|
||||||
do {
|
do {
|
||||||
_ = try TunnelKitProvider.Configuration.parsed(from: url, stripped: &lines)
|
let parsedFile = try TunnelKitProvider.Configuration.parsed(from: url, returnsStripped: true)
|
||||||
if let attachment = lines.joined(separator: "\n").data(using: .utf8) {
|
if let attachment = parsedFile.strippedLines?.joined(separator: "\n").data(using: .utf8) {
|
||||||
vc.addAttachmentData(attachment, mimeType: AppConstants.IssueReporter.MIME.configuration, fileName: AppConstants.IssueReporter.Filenames.configuration)
|
vc.addAttachmentData(attachment, mimeType: AppConstants.IssueReporter.MIME.configuration, fileName: AppConstants.IssueReporter.Filenames.configuration)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
//
|
||||||
|
// ParsedFile+Alerts.swift
|
||||||
|
// Passepartout-iOS
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 10/27/18.
|
||||||
|
// Copyright (c) 2018 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/keeshux
|
||||||
|
//
|
||||||
|
// 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 Foundation
|
||||||
|
import UIKit
|
||||||
|
import TunnelKit
|
||||||
|
import SwiftyBeaver
|
||||||
|
|
||||||
|
private let log = SwiftyBeaver.self
|
||||||
|
|
||||||
|
extension ParsedFile {
|
||||||
|
static func from(_ url: URL, withErrorAlertIn viewController: UIViewController) -> ParsedFile? {
|
||||||
|
log.debug("Parsing configuration URL: \(url)")
|
||||||
|
do {
|
||||||
|
return try TunnelKitProvider.Configuration.parsed(from: url)
|
||||||
|
} catch ApplicationError.missingConfiguration(let option) {
|
||||||
|
log.error("Could not parse configuration URL: missing configuration, \(option)")
|
||||||
|
let message = L10n.ParsedFile.Alerts.Missing.message(option)
|
||||||
|
alertConfigurationImportError(url: url, in: viewController, withMessage: message)
|
||||||
|
} catch ApplicationError.unsupportedConfiguration(let option) {
|
||||||
|
log.error("Could not parse configuration URL: unsupported configuration, \(option)")
|
||||||
|
let message = L10n.ParsedFile.Alerts.Unsupported.message(option)
|
||||||
|
alertConfigurationImportError(url: url, in: viewController, withMessage: message)
|
||||||
|
} catch let e {
|
||||||
|
log.error("Could not parse configuration URL: \(e)")
|
||||||
|
let message = L10n.ParsedFile.Alerts.Parsing.message(e.localizedDescription)
|
||||||
|
alertConfigurationImportError(url: url, in: viewController, withMessage: message)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func alertConfigurationImportError(url: URL, in vc: UIViewController, withMessage message: String) {
|
||||||
|
let alert = Macros.alert(url.normalizedFilename, message)
|
||||||
|
// alert.addDefaultAction(L10n.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.Global.ok)
|
||||||
|
vc.present(alert, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,20 +117,20 @@ class ConfigurationViewController: UIViewController, TableModelHost {
|
||||||
// MARK: Actions
|
// MARK: Actions
|
||||||
|
|
||||||
private func resetOriginalConfiguration() {
|
private func resetOriginalConfiguration() {
|
||||||
guard let url = originalConfigurationURL else {
|
guard let originalURL = originalConfigurationURL else {
|
||||||
log.warning("Resetting with no original configuration set? Bad table model?")
|
log.warning("Resetting with no original configuration set? Bad table model?")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let originalConfiguration: TunnelKitProvider.Configuration
|
let parsedFile: ParsedFile
|
||||||
do {
|
do {
|
||||||
(_, originalConfiguration) = try TunnelKitProvider.Configuration.parsed(from: url)
|
parsedFile = try TunnelKitProvider.Configuration.parsed(from: originalURL)
|
||||||
} catch let e {
|
} catch let e {
|
||||||
log.warning("Could not parse original configuration: \(e)")
|
log.error("Could not parse original configuration: \(e)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
initialConfiguration = originalConfiguration.sessionConfiguration
|
configuration = parsedFile.configuration.sessionConfiguration.builder()
|
||||||
configuration = initialConfiguration.builder()
|
itemRefresh.isEnabled = !configuration.canCommunicate(with: initialConfiguration)
|
||||||
itemRefresh.isEnabled = true // allow for manual reconnection
|
initialConfiguration = parsedFile.configuration.sessionConfiguration
|
||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
|
|
||||||
delegate?.configuration(didUpdate: initialConfiguration)
|
delegate?.configuration(didUpdate: initialConfiguration)
|
||||||
|
|
|
@ -30,26 +30,13 @@ import SwiftyBeaver
|
||||||
private let log = SwiftyBeaver.self
|
private let log = SwiftyBeaver.self
|
||||||
|
|
||||||
class WizardHostViewController: UITableViewController, TableModelHost, Wizard {
|
class WizardHostViewController: UITableViewController, TableModelHost, Wizard {
|
||||||
private struct ParsedFile {
|
|
||||||
let url: URL
|
|
||||||
|
|
||||||
var filename: String {
|
|
||||||
let raw = url.deletingPathExtension().lastPathComponent
|
|
||||||
return raw.components(separatedBy: AppConstants.Store.filenameCharset.inverted).joined(separator: "_")
|
|
||||||
}
|
|
||||||
|
|
||||||
let hostname: String
|
|
||||||
|
|
||||||
let configuration: TunnelKitProvider.Configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBOutlet private weak var itemNext: UIBarButtonItem!
|
@IBOutlet private weak var itemNext: UIBarButtonItem!
|
||||||
|
|
||||||
private let existingHosts: [String] = {
|
private let existingHosts: [String] = {
|
||||||
return TransientStore.shared.service.ids(forContext: .host).sorted()
|
return TransientStore.shared.service.ids(forContext: .host).sorted()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private var parsedFile: ParsedFile? {
|
var parsedFile: ParsedFile? {
|
||||||
didSet {
|
didSet {
|
||||||
useSuggestedTitle()
|
useSuggestedTitle()
|
||||||
}
|
}
|
||||||
|
@ -99,26 +86,12 @@ class WizardHostViewController: UITableViewController, TableModelHost, Wizard {
|
||||||
|
|
||||||
// MARK: Actions
|
// MARK: Actions
|
||||||
|
|
||||||
func setConfigurationURL(_ url: URL) throws {
|
|
||||||
log.debug("Parsing configuration URL: \(url)")
|
|
||||||
|
|
||||||
let hostname: String
|
|
||||||
let configuration: TunnelKitProvider.Configuration
|
|
||||||
do {
|
|
||||||
(hostname, configuration) = try TunnelKitProvider.Configuration.parsed(from: url)
|
|
||||||
} catch let e {
|
|
||||||
log.error("Could not parse .ovpn configuration file: \(e)")
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
parsedFile = ParsedFile(url: url, hostname: hostname, configuration: configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func useSuggestedTitle() {
|
private func useSuggestedTitle() {
|
||||||
guard let field = cellTitle?.field else {
|
guard let field = cellTitle?.field else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if field.text?.isEmpty ?? true {
|
if field.text?.isEmpty ?? true {
|
||||||
field.text = parsedFile?.filename
|
field.text = parsedFile?.url.normalizedFilename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +189,7 @@ extension WizardHostViewController {
|
||||||
let cell = Cells.field.dequeue(from: tableView, for: indexPath)
|
let cell = Cells.field.dequeue(from: tableView, for: indexPath)
|
||||||
cell.caption = L10n.Wizards.Host.Cells.TitleInput.caption
|
cell.caption = L10n.Wizards.Host.Cells.TitleInput.caption
|
||||||
cell.captionWidth = 100.0
|
cell.captionWidth = 100.0
|
||||||
cell.allowedCharset = AppConstants.Store.filenameCharset
|
cell.allowedCharset = .filename
|
||||||
cell.field.placeholder = L10n.Wizards.Host.Cells.TitleInput.placeholder
|
cell.field.placeholder = L10n.Wizards.Host.Cells.TitleInput.placeholder
|
||||||
cell.field.clearButtonMode = .always
|
cell.field.clearButtonMode = .always
|
||||||
cell.field.returnKeyType = .done
|
cell.field.returnKeyType = .done
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
0E89DFD0213F223400741BA1 /* Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCF213F223400741BA1 /* Wizard.swift */; };
|
0E89DFD0213F223400741BA1 /* Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCF213F223400741BA1 /* Wizard.swift */; };
|
||||||
0E8D97E221388B52006FB4A0 /* InfrastructurePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */; };
|
0E8D97E221388B52006FB4A0 /* InfrastructurePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */; };
|
||||||
0E8D97E521389277006FB4A0 /* pia.json in Resources */ = {isa = PBXBuildFile; fileRef = 0E8D97E421389276006FB4A0 /* pia.json */; };
|
0E8D97E521389277006FB4A0 /* pia.json in Resources */ = {isa = PBXBuildFile; fileRef = 0E8D97E421389276006FB4A0 /* pia.json */; };
|
||||||
|
0EA068F4218475F800C320AD /* ParsedFile+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */; };
|
||||||
0EAAD71920E6669A0088754A /* GroupConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE8DED20C93E4C004C739C /* GroupConstants.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 */; };
|
0EB60FDA2111136E00AD27F3 /* UITextView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */; };
|
||||||
0EBBE8F221822B4D00106008 /* ConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F021822B4D00106008 /* ConnectionServiceTests.swift */; };
|
0EBBE8F221822B4D00106008 /* ConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F021822B4D00106008 /* ConnectionServiceTests.swift */; };
|
||||||
|
@ -166,6 +167,7 @@
|
||||||
0E89DFCF213F223400741BA1 /* Wizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wizard.swift; sourceTree = "<group>"; };
|
0E89DFCF213F223400741BA1 /* Wizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wizard.swift; sourceTree = "<group>"; };
|
||||||
0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfrastructurePreset.swift; sourceTree = "<group>"; };
|
0E8D97E121388B52006FB4A0 /* InfrastructurePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfrastructurePreset.swift; sourceTree = "<group>"; };
|
||||||
0E8D97E421389276006FB4A0 /* pia.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pia.json; sourceTree = "<group>"; };
|
0E8D97E421389276006FB4A0 /* pia.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pia.json; sourceTree = "<group>"; };
|
||||||
|
0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParsedFile+Alerts.swift"; sourceTree = "<group>"; };
|
||||||
0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Search.swift"; sourceTree = "<group>"; };
|
0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Search.swift"; sourceTree = "<group>"; };
|
||||||
0EBBE8F021822B4D00106008 /* ConnectionServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionServiceTests.swift; sourceTree = "<group>"; };
|
0EBBE8F021822B4D00106008 /* ConnectionServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionServiceTests.swift; sourceTree = "<group>"; };
|
||||||
0EBBE8F121822B4D00106008 /* ConnectionService.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ConnectionService.json; sourceTree = "<group>"; };
|
0EBBE8F121822B4D00106008 /* ConnectionService.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ConnectionService.json; sourceTree = "<group>"; };
|
||||||
|
@ -435,6 +437,7 @@
|
||||||
0EFD943D215BE10800529B64 /* IssueReporter.swift */,
|
0EFD943D215BE10800529B64 /* IssueReporter.swift */,
|
||||||
0E4FD7F020D58618002221FF /* Macros.swift */,
|
0E4FD7F020D58618002221FF /* Macros.swift */,
|
||||||
0ED38AE9214054A50004D387 /* OptionViewController.swift */,
|
0ED38AE9214054A50004D387 /* OptionViewController.swift */,
|
||||||
|
0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */,
|
||||||
0EDE8DE320C89028004C739C /* SwiftGen+Storyboards.swift */,
|
0EDE8DE320C89028004C739C /* SwiftGen+Storyboards.swift */,
|
||||||
0E05C61C20D27C82006EE732 /* Theme.swift */,
|
0E05C61C20D27C82006EE732 /* Theme.swift */,
|
||||||
0ECEE44F20E1182E00A6BB43 /* Theme+Cells.swift */,
|
0ECEE44F20E1182E00A6BB43 /* Theme+Cells.swift */,
|
||||||
|
@ -852,6 +855,7 @@
|
||||||
0E57F63E20C83FC5008323CF /* ServiceViewController.swift in Sources */,
|
0E57F63E20C83FC5008323CF /* ServiceViewController.swift in Sources */,
|
||||||
0E39BCF0214B9EF10035E9DE /* WebServices.swift in Sources */,
|
0E39BCF0214B9EF10035E9DE /* WebServices.swift in Sources */,
|
||||||
0EDE8DE720C93945004C739C /* Credentials.swift in Sources */,
|
0EDE8DE720C93945004C739C /* Credentials.swift in Sources */,
|
||||||
|
0EA068F4218475F800C320AD /* ParsedFile+Alerts.swift in Sources */,
|
||||||
0ED38AF2214177920004D387 /* VPNProvider.swift in Sources */,
|
0ED38AF2214177920004D387 /* VPNProvider.swift in Sources */,
|
||||||
0E4C9CB920DB9BC600A0C59C /* TrustedNetworks.swift in Sources */,
|
0E4C9CB920DB9BC600A0C59C /* TrustedNetworks.swift in Sources */,
|
||||||
0E57F63C20C83FC5008323CF /* AppDelegate.swift in Sources */,
|
0E57F63C20C83FC5008323CF /* AppDelegate.swift in Sources */,
|
||||||
|
|
|
@ -52,10 +52,11 @@
|
||||||
"wizards.host.cells.title_input.placeholder" = "My Profile";
|
"wizards.host.cells.title_input.placeholder" = "My Profile";
|
||||||
"wizards.host.sections.existing.header" = "Existing profiles";
|
"wizards.host.sections.existing.header" = "Existing profiles";
|
||||||
"wizards.host.alerts.existing.message" = "A host profile with the same title already exists. Replace it?";
|
"wizards.host.alerts.existing.message" = "A host profile with the same title already exists. Replace it?";
|
||||||
"wizards.host.alerts.missing.message" = "The configuration file lacks a required option (%@).";
|
|
||||||
"wizards.host.alerts.unsupported.message" = "The configuration file contains an unsupported option (%@).";
|
"parsed_file.alerts.missing.message" = "The configuration file lacks a required option (%@).";
|
||||||
"wizards.host.alerts.parsing.message" = "Unable to parse the provided configuration file (%@).";
|
"parsed_file.alerts.unsupported.message" = "The configuration file contains an unsupported option (%@).";
|
||||||
"wizards.host.alerts.buttons.report" = "Report an issue";
|
"parsed_file.alerts.parsing.message" = "Unable to parse the provided configuration file (%@).";
|
||||||
|
"parsed_file.alerts.buttons.report" = "Report an issue";
|
||||||
|
|
||||||
"service.welcome.message" = "Welcome to Passepartout!\n\nUse the organizer to add a new profile.";
|
"service.welcome.message" = "Welcome to Passepartout!\n\nUse the organizer to add a new profile.";
|
||||||
"service.sections.general.header" = "General";
|
"service.sections.general.header" = "General";
|
||||||
|
|
|
@ -40,16 +40,6 @@ class AppConstants {
|
||||||
static let providersDirectory = "Providers"
|
static let providersDirectory = "Providers"
|
||||||
|
|
||||||
static let hostsDirectory = "Hosts"
|
static let hostsDirectory = "Hosts"
|
||||||
|
|
||||||
static let filenameCharset: CharacterSet = {
|
|
||||||
var chars: CharacterSet = .decimalDigits
|
|
||||||
let english = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
let symbols = "-_"
|
|
||||||
chars.formUnion(CharacterSet(charactersIn: english))
|
|
||||||
chars.formUnion(CharacterSet(charactersIn: english.lowercased()))
|
|
||||||
chars.formUnion(CharacterSet(charactersIn: symbols))
|
|
||||||
return chars
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class VPN {
|
class VPN {
|
||||||
|
|
|
@ -44,6 +44,6 @@ extension ConnectionService {
|
||||||
|
|
||||||
private func targetConfigurationURL(for profile: ConnectionProfile) -> URL {
|
private func targetConfigurationURL(for profile: ConnectionProfile) -> URL {
|
||||||
let contextURL = ConnectionService.ProfileKey(profile).contextURL(in: self)
|
let contextURL = ConnectionService.ProfileKey(profile).contextURL(in: self)
|
||||||
return contextURL.appendingPathComponent("\(profile.id).ovpn")
|
return contextURL.appendingPathComponent(profile.id).appendingPathExtension("ovpn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ extension ConnectionService {
|
||||||
// log.verbose(String(data: newData, encoding: .utf8)!)
|
// log.verbose(String(data: newData, encoding: .utf8)!)
|
||||||
try newData.write(to: to)
|
try newData.write(to: to)
|
||||||
} catch let e {
|
} catch let e {
|
||||||
log.warning("Could not migrate service: \(e)")
|
log.error("Could not migrate service: \(e)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ extension ConnectionService {
|
||||||
// provider["id"] = id
|
// provider["id"] = id
|
||||||
// provider.removeValue(forKey: "name")
|
// provider.removeValue(forKey: "name")
|
||||||
|
|
||||||
let url = providersParentURL.appendingPathComponent("\(id).json")
|
let url = providersParentURL.appendingPathComponent(id).appendingPathExtension("json")
|
||||||
let data = try JSONSerialization.data(withJSONObject: provider, options: [])
|
let data = try JSONSerialization.data(withJSONObject: provider, options: [])
|
||||||
try data.write(to: url)
|
try data.write(to: url)
|
||||||
} else if var host = p["host"] as? [String: Any] {
|
} else if var host = p["host"] as? [String: Any] {
|
||||||
|
@ -155,7 +155,7 @@ extension ConnectionService {
|
||||||
// host["id"] = id
|
// host["id"] = id
|
||||||
// host.removeValue(forKey: "title")
|
// host.removeValue(forKey: "title")
|
||||||
|
|
||||||
let url = hostsParentURL.appendingPathComponent("\(id).json")
|
let url = hostsParentURL.appendingPathComponent(id).appendingPathExtension("json")
|
||||||
let data = try JSONSerialization.data(withJSONObject: host, options: [])
|
let data = try JSONSerialization.data(withJSONObject: host, options: [])
|
||||||
try data.write(to: url)
|
try data.write(to: url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -221,7 +221,7 @@ class ConnectionService: Codable {
|
||||||
cache[key] = PlaceholderConnectionProfile(key)
|
cache[key] = PlaceholderConnectionProfile(key)
|
||||||
}
|
}
|
||||||
} catch let e {
|
} catch let e {
|
||||||
log.warning("Could not list provider contents: \(e) (\(providersURL))")
|
log.error("Could not list provider contents: \(e) (\(providersURL))")
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let files = try fm.contentsOfDirectory(at: hostsURL, includingPropertiesForKeys: nil, options: [])
|
let files = try fm.contentsOfDirectory(at: hostsURL, includingPropertiesForKeys: nil, options: [])
|
||||||
|
@ -234,7 +234,7 @@ class ConnectionService: Codable {
|
||||||
cache[key] = PlaceholderConnectionProfile(key)
|
cache[key] = PlaceholderConnectionProfile(key)
|
||||||
}
|
}
|
||||||
} catch let e {
|
} catch let e {
|
||||||
log.warning("Could not list host contents: \(e) (\(hostsURL))")
|
log.error("Could not list host contents: \(e) (\(hostsURL))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ class ConnectionService: Codable {
|
||||||
try data.write(to: url)
|
try data.write(to: url)
|
||||||
log.debug("Saved provider '\(profile.id)'")
|
log.debug("Saved provider '\(profile.id)'")
|
||||||
} catch let e {
|
} catch let e {
|
||||||
log.warning("Could not save provider '\(profile.id)': \(e)")
|
log.error("Could not save provider '\(profile.id)': \(e)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else if let profile = entry as? HostConnectionProfile {
|
} else if let profile = entry as? HostConnectionProfile {
|
||||||
|
@ -267,7 +267,7 @@ class ConnectionService: Codable {
|
||||||
try data.write(to: url)
|
try data.write(to: url)
|
||||||
log.debug("Saved host '\(profile.id)'")
|
log.debug("Saved host '\(profile.id)'")
|
||||||
} catch let e {
|
} catch let e {
|
||||||
log.warning("Could not save host '\(profile.id)': \(e)")
|
log.error("Could not save host '\(profile.id)': \(e)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else if let placeholder = entry as? PlaceholderConnectionProfile {
|
} else if let placeholder = entry as? PlaceholderConnectionProfile {
|
||||||
|
@ -292,7 +292,7 @@ class ConnectionService: Codable {
|
||||||
}
|
}
|
||||||
cache[key] = profile
|
cache[key] = profile
|
||||||
} catch let e {
|
} catch let e {
|
||||||
log.warning("Could not decode profile JSON: \(e)")
|
log.error("Could not decode profile JSON: \(e)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -304,15 +304,14 @@ class ConnectionService: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func profileId(fromURL url: URL) -> String? {
|
private static func profileId(fromURL url: URL) -> String? {
|
||||||
let filename = url.lastPathComponent
|
guard url.pathExtension == "json" else {
|
||||||
guard let extRange = filename.range(of: ".json") else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return String(filename[filename.startIndex..<extRange.lowerBound])
|
return url.deletingPathExtension().lastPathComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func url(in directory: URL, forProfileId profileId: String) -> URL {
|
private static func url(in directory: URL, forProfileId profileId: String) -> URL {
|
||||||
return directory.appendingPathComponent("\(profileId).json")
|
return directory.appendingPathComponent(profileId).appendingPathExtension("json")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Profiles
|
// MARK: Profiles
|
||||||
|
|
|
@ -203,7 +203,7 @@ class InfrastructureFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cacheURL(for name: Infrastructure.Name) -> URL {
|
private func cacheURL(for name: Infrastructure.Name) -> URL {
|
||||||
return cachePath.appendingPathComponent("\(name.webName).json")
|
return cachePath.appendingPathComponent(name.webName).appendingPathExtension("json")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cacheModificationDate(for name: Infrastructure.Name) -> Date? {
|
private func cacheModificationDate(for name: Infrastructure.Name) -> Date? {
|
||||||
|
|
|
@ -404,6 +404,38 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum ParsedFile {
|
||||||
|
|
||||||
|
internal enum Alerts {
|
||||||
|
|
||||||
|
internal enum Buttons {
|
||||||
|
/// Report an issue
|
||||||
|
internal static let report = L10n.tr("Localizable", "parsed_file.alerts.buttons.report")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum Missing {
|
||||||
|
/// The configuration file lacks a required option (%@).
|
||||||
|
internal static func message(_ p1: String) -> String {
|
||||||
|
return L10n.tr("Localizable", "parsed_file.alerts.missing.message", p1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum Parsing {
|
||||||
|
/// Unable to parse the provided configuration file (%@).
|
||||||
|
internal static func message(_ p1: String) -> String {
|
||||||
|
return L10n.tr("Localizable", "parsed_file.alerts.parsing.message", p1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum Unsupported {
|
||||||
|
/// The configuration file contains an unsupported option (%@).
|
||||||
|
internal static func message(_ p1: String) -> String {
|
||||||
|
return L10n.tr("Localizable", "parsed_file.alerts.unsupported.message", p1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal enum Provider {
|
internal enum Provider {
|
||||||
|
|
||||||
internal enum Preset {
|
internal enum Preset {
|
||||||
|
@ -734,36 +766,10 @@ internal enum L10n {
|
||||||
|
|
||||||
internal enum Alerts {
|
internal enum Alerts {
|
||||||
|
|
||||||
internal enum Buttons {
|
|
||||||
/// Report an issue
|
|
||||||
internal static let report = L10n.tr("Localizable", "wizards.host.alerts.buttons.report")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum Existing {
|
internal enum Existing {
|
||||||
/// A host profile with the same title already exists. Replace it?
|
/// A host profile with the same title already exists. Replace it?
|
||||||
internal static let message = L10n.tr("Localizable", "wizards.host.alerts.existing.message")
|
internal static let message = L10n.tr("Localizable", "wizards.host.alerts.existing.message")
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum Missing {
|
|
||||||
/// The configuration file lacks a required option (%@).
|
|
||||||
internal static func message(_ p1: String) -> String {
|
|
||||||
return L10n.tr("Localizable", "wizards.host.alerts.missing.message", p1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum Parsing {
|
|
||||||
/// Unable to parse the provided configuration file (%@).
|
|
||||||
internal static func message(_ p1: String) -> String {
|
|
||||||
return L10n.tr("Localizable", "wizards.host.alerts.parsing.message", p1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum Unsupported {
|
|
||||||
/// The configuration file contains an unsupported option (%@).
|
|
||||||
internal static func message(_ p1: String) -> String {
|
|
||||||
return L10n.tr("Localizable", "wizards.host.alerts.unsupported.message", p1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum Cells {
|
internal enum Cells {
|
||||||
|
|
|
@ -201,3 +201,24 @@ extension String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension CharacterSet {
|
||||||
|
static let filename: CharacterSet = {
|
||||||
|
var chars: CharacterSet = .decimalDigits
|
||||||
|
let english = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
let symbols = "-_"
|
||||||
|
chars.formUnion(CharacterSet(charactersIn: english))
|
||||||
|
chars.formUnion(CharacterSet(charactersIn: english.lowercased()))
|
||||||
|
chars.formUnion(CharacterSet(charactersIn: symbols))
|
||||||
|
return chars
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension URL {
|
||||||
|
private static let illegalCharacterFallback = "_"
|
||||||
|
|
||||||
|
var normalizedFilename: String {
|
||||||
|
let filename = deletingPathExtension().lastPathComponent
|
||||||
|
return filename.components(separatedBy: CharacterSet.filename.inverted).joined(separator: URL.illegalCharacterFallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,16 @@ import SwiftyBeaver
|
||||||
|
|
||||||
private let log = SwiftyBeaver.self
|
private let log = SwiftyBeaver.self
|
||||||
|
|
||||||
|
struct ParsedFile {
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
let hostname: String
|
||||||
|
|
||||||
|
let configuration: TunnelKitProvider.Configuration
|
||||||
|
|
||||||
|
let strippedLines: [String]?
|
||||||
|
}
|
||||||
|
|
||||||
extension TunnelKitProvider.Configuration {
|
extension TunnelKitProvider.Configuration {
|
||||||
private struct Regex {
|
private struct Regex {
|
||||||
static let proto = Utils.regex("^proto +(udp6?|tcp6?)")
|
static let proto = Utils.regex("^proto +(udp6?|tcp6?)")
|
||||||
|
@ -49,21 +59,25 @@ extension TunnelKitProvider.Configuration {
|
||||||
|
|
||||||
static let renegSec = Utils.regex("^reneg-sec +\\d+")
|
static let renegSec = Utils.regex("^reneg-sec +\\d+")
|
||||||
|
|
||||||
static let fragment = Utils.regex("^fragment +\\d+")
|
|
||||||
|
|
||||||
static let proxy = Utils.regex("^\\w+-proxy")
|
|
||||||
|
|
||||||
static let keyDirection = Utils.regex("^key-direction +\\d")
|
static let keyDirection = Utils.regex("^key-direction +\\d")
|
||||||
|
|
||||||
static let externalFiles = Utils.regex("^(ca|cert|key|tls-auth|tls-crypt) ")
|
|
||||||
|
|
||||||
static let blockBegin = Utils.regex("^<[\\w\\-]+>")
|
static let blockBegin = Utils.regex("^<[\\w\\-]+>")
|
||||||
|
|
||||||
static let blockEnd = Utils.regex("^<\\/[\\w\\-]+>")
|
static let blockEnd = Utils.regex("^<\\/[\\w\\-]+>")
|
||||||
|
|
||||||
|
// unsupported
|
||||||
|
|
||||||
|
// static let fragment = Utils.regex("^fragment +\\d+")
|
||||||
|
static let fragment = Utils.regex("^fragment")
|
||||||
|
|
||||||
|
static let proxy = Utils.regex("^\\w+-proxy")
|
||||||
|
|
||||||
|
static let externalFiles = Utils.regex("^(ca|cert|key|tls-auth|tls-crypt) ")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func parsed(from url: URL, stripped: UnsafeMutablePointer<[String]>? = nil) throws -> (String, TunnelKitProvider.Configuration) {
|
static func parsed(from url: URL, returnsStripped: Bool = false) throws -> ParsedFile {
|
||||||
let lines = try String(contentsOf: url).trimmedLines()
|
let lines = try String(contentsOf: url).trimmedLines()
|
||||||
|
var strippedLines: [String]? = returnsStripped ? [] : nil
|
||||||
|
|
||||||
var defaultProto: TunnelKitProvider.SocketType?
|
var defaultProto: TunnelKitProvider.SocketType?
|
||||||
var defaultPort: UInt16?
|
var defaultPort: UInt16?
|
||||||
|
@ -94,7 +108,7 @@ extension TunnelKitProvider.Configuration {
|
||||||
var strippedLine = line
|
var strippedLine = line
|
||||||
defer {
|
defer {
|
||||||
if isHandled {
|
if isHandled {
|
||||||
stripped?.pointee.append(strippedLine)
|
strippedLines?.append(strippedLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +325,7 @@ extension TunnelKitProvider.Configuration {
|
||||||
var builder = TunnelKitProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build())
|
var builder = TunnelKitProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build())
|
||||||
builder.endpointProtocols = endpointProtocols
|
builder.endpointProtocols = endpointProtocols
|
||||||
|
|
||||||
return (hostname, builder.build())
|
return ParsedFile(url: url, hostname: hostname, configuration: builder.build(), strippedLines: strippedLines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,4 +58,23 @@ class ConnectionServiceTests: XCTestCase {
|
||||||
XCTAssert(activeProfile.parameters.sessionConfiguration.cipher == .aes256cbc)
|
XCTAssert(activeProfile.parameters.sessionConfiguration.cipher == .aes256cbc)
|
||||||
XCTAssert(activeProfile.parameters.sessionConfiguration.ca.pem == "bogus+ca")
|
XCTAssert(activeProfile.parameters.sessionConfiguration.ca.pem == "bogus+ca")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPathExtension() {
|
||||||
|
XCTAssertTrue(privateTestPathExtension("file:///foo/bar/johndoe.json"))
|
||||||
|
XCTAssertFalse(privateTestPathExtension("file:///foo/bar/break.json.johndoe.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func privateTestPathExtension(_ string: String) -> Bool {
|
||||||
|
let url = URL(string: string)!
|
||||||
|
let filename = url.lastPathComponent
|
||||||
|
guard let extRange = filename.range(of: ".json") else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard url.pathExtension == "json" else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let name1 = String(filename[filename.startIndex..<extRange.lowerBound])
|
||||||
|
let name2 = url.deletingPathExtension().lastPathComponent
|
||||||
|
return name1 == name2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,16 +39,15 @@ class FileConfigurationTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPIA() throws {
|
func testPIA() throws {
|
||||||
let cfg = try TunnelKitProvider.Configuration.parsed(from: url(withName: "pia-hungary")).1
|
let cfg = try TunnelKitProvider.Configuration.parsed(from: url(withName: "pia-hungary")).configuration
|
||||||
XCTAssertEqual(cfg.sessionConfiguration.cipher, .aes128cbc)
|
XCTAssertEqual(cfg.sessionConfiguration.cipher, .aes128cbc)
|
||||||
XCTAssertEqual(cfg.sessionConfiguration.digest, .sha1)
|
XCTAssertEqual(cfg.sessionConfiguration.digest, .sha1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testStripped() throws {
|
func testStripped() throws {
|
||||||
var lines: [String] = []
|
let lines = try TunnelKitProvider.Configuration.parsed(from: url(withName: "pia-hungary"), returnsStripped: true).strippedLines!
|
||||||
_ = try TunnelKitProvider.Configuration.parsed(from: url(withName: "pia-hungary"), stripped: &lines)
|
let stripped = lines.joined(separator: "\n")
|
||||||
let cfg = lines.joined(separator: "\n")
|
print(stripped)
|
||||||
print(cfg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func url(withName name: String) -> URL {
|
private func url(withName name: String) -> URL {
|
||||||
|
|
Loading…
Reference in New Issue