Merge pull request #13 from keeshux/attach-ovpn-to-report
Attach .ovpn to connectivity issue report
This commit is contained in:
commit
02c8e7b6ea
|
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Attach .ovpn when reporting a connectivity issue, stripped of sensitive data. [#13](https://github.com/keeshux/passepartout-ios/pull/13)
|
||||
|
||||
## 1.0 beta 1107 (2018-10-26)
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -118,17 +118,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
|
|||
}
|
||||
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 alert = Macros.alert(L10n.Organizer.Sections.Hosts.header, L10n.Wizards.Host.Alerts.unsupported(option))
|
||||
alert.addCancelAction(L10n.Global.ok)
|
||||
root.present(alert, animated: true, completion: nil)
|
||||
let message = L10n.Wizards.Host.Alerts.Unsupported.message(option)
|
||||
alertConfigurationImportError(url: url, in: root, withMessage: message)
|
||||
} catch let e {
|
||||
let alert = Macros.alert(L10n.Organizer.Sections.Hosts.header, L10n.Wizards.Host.Alerts.parsing(e.localizedDescription))
|
||||
alert.addCancelAction(L10n.Global.ok)
|
||||
root.present(alert, animated: true, completion: nil)
|
||||
let message = L10n.Wizards.Host.Alerts.Parsing.message(e.localizedDescription)
|
||||
alertConfigurationImportError(url: url, in: root, withMessage: message)
|
||||
}
|
||||
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 {
|
||||
|
|
|
@ -24,9 +24,28 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import TunnelKit
|
||||
import MessageUI
|
||||
|
||||
class IssueReporter: NSObject {
|
||||
struct Attachments {
|
||||
let debugLog: Bool
|
||||
|
||||
let configurationURL: URL?
|
||||
|
||||
var description: String?
|
||||
|
||||
init(debugLog: Bool, configurationURL: URL?) {
|
||||
self.debugLog = debugLog
|
||||
self.configurationURL = configurationURL
|
||||
}
|
||||
|
||||
init(debugLog: Bool, profile: ConnectionProfile) {
|
||||
let url = TransientStore.shared.service.configurationURL(for: profile)
|
||||
self.init(debugLog: debugLog, configurationURL: url)
|
||||
}
|
||||
}
|
||||
|
||||
static let shared = IssueReporter()
|
||||
|
||||
private weak var viewController: UIViewController?
|
||||
|
@ -35,7 +54,7 @@ class IssueReporter: NSObject {
|
|||
super.init()
|
||||
}
|
||||
|
||||
func present(in viewController: UIViewController) {
|
||||
func present(in viewController: UIViewController, withAttachments attachments: Attachments) {
|
||||
guard MFMailComposeViewController.canSendMail() else {
|
||||
let alert = Macros.alert(L10n.IssueReporter.title, L10n.IssueReporter.Alerts.EmailNotConfigured.message)
|
||||
alert.addCancelAction(L10n.Global.ok)
|
||||
|
@ -45,26 +64,40 @@ class IssueReporter: NSObject {
|
|||
|
||||
self.viewController = viewController
|
||||
|
||||
let alert = Macros.alert(L10n.IssueReporter.title, L10n.IssueReporter.message)
|
||||
alert.addDefaultAction(L10n.IssueReporter.Buttons.accept) {
|
||||
VPN.shared.requestDebugLog(fallback: AppConstants.Log.debugSnapshot) {
|
||||
self.composeEmail(withDebugLog: $0)
|
||||
if attachments.debugLog {
|
||||
let alert = Macros.alert(L10n.IssueReporter.title, L10n.IssueReporter.message)
|
||||
alert.addDefaultAction(L10n.IssueReporter.Buttons.accept) {
|
||||
VPN.shared.requestDebugLog(fallback: AppConstants.Log.debugSnapshot) {
|
||||
self.composeEmail(withDebugLog: $0, configurationURL: attachments.configurationURL, description: attachments.description)
|
||||
}
|
||||
}
|
||||
alert.addCancelAction(L10n.Global.cancel)
|
||||
viewController.present(alert, animated: true, completion: nil)
|
||||
} else {
|
||||
composeEmail(withDebugLog: nil, configurationURL: attachments.configurationURL, description: attachments.description)
|
||||
}
|
||||
alert.addCancelAction(L10n.Global.cancel)
|
||||
viewController.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func composeEmail(withDebugLog debugLog: String?) {
|
||||
private func composeEmail(withDebugLog debugLog: String?, configurationURL: URL?, description: String?) {
|
||||
let metadata = DebugLog(raw: "--").decoratedString()
|
||||
|
||||
let vc = MFMailComposeViewController()
|
||||
vc.setToRecipients([AppConstants.IssueReporter.recipient])
|
||||
vc.setSubject(L10n.IssueReporter.Email.subject(GroupConstants.App.name))
|
||||
vc.setMessageBody(L10n.IssueReporter.Email.body(metadata), isHTML: false)
|
||||
vc.setMessageBody(L10n.IssueReporter.Email.body(description ?? L10n.IssueReporter.Email.description, metadata), isHTML: false)
|
||||
if let raw = debugLog {
|
||||
let attachment = DebugLog(raw: raw).decoratedData()
|
||||
vc.addAttachmentData(attachment, mimeType: AppConstants.IssueReporter.attachmentMIME, fileName: AppConstants.Log.debugFilename)
|
||||
vc.addAttachmentData(attachment, mimeType: AppConstants.IssueReporter.MIME.debugLog, fileName: AppConstants.IssueReporter.Filenames.debugLog)
|
||||
}
|
||||
if let url = configurationURL {
|
||||
var lines: [String] = []
|
||||
do {
|
||||
_ = try TunnelKitProvider.Configuration.parsed(from: url, stripped: &lines)
|
||||
if let attachment = lines.joined(separator: "\n").data(using: .utf8) {
|
||||
vc.addAttachmentData(attachment, mimeType: AppConstants.IssueReporter.MIME.configuration, fileName: AppConstants.IssueReporter.Filenames.configuration)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
vc.mailComposeDelegate = self
|
||||
vc.apply(Theme.current)
|
||||
|
|
|
@ -64,7 +64,7 @@ class DebugLogViewController: UIViewController {
|
|||
}
|
||||
let data = DebugLog(raw: raw).decoratedData()
|
||||
|
||||
let path = NSTemporaryDirectory().appending(AppConstants.Log.debugFilename)
|
||||
let path = NSTemporaryDirectory().appending(AppConstants.IssueReporter.Filenames.debugLog)
|
||||
let url = URL(fileURLWithPath: path)
|
||||
do {
|
||||
try data.write(to: url)
|
||||
|
|
|
@ -134,7 +134,7 @@ class WizardHostViewController: UITableViewController, TableModelHost, Wizard {
|
|||
profile.parameters = file.configuration
|
||||
|
||||
guard !TransientStore.shared.service.containsProfile(profile) else {
|
||||
let alert = Macros.alert(title, L10n.Wizards.Host.Alerts.existing)
|
||||
let alert = Macros.alert(title, L10n.Wizards.Host.Alerts.Existing.message)
|
||||
alert.addDefaultAction(L10n.Global.ok) {
|
||||
self.next(withProfile: profile)
|
||||
}
|
||||
|
@ -159,8 +159,8 @@ class WizardHostViewController: UITableViewController, TableModelHost, Wizard {
|
|||
}
|
||||
if let url = parsedFile?.url {
|
||||
do {
|
||||
let savedUrl = try ProfileConfigurationFactory.shared.save(url: url, for: profile)
|
||||
log.debug("Associated .ovpn configuration file to profile '\(profile.id)': \(savedUrl)")
|
||||
let savedURL = try TransientStore.shared.service.save(configurationURL: url, for: profile)
|
||||
log.debug("Associated .ovpn configuration file to profile '\(profile.id)': \(savedURL)")
|
||||
} catch let e {
|
||||
log.error("Could not associate .ovpn configuration file to profile: \(e)")
|
||||
}
|
||||
|
|
|
@ -154,7 +154,7 @@ class ServiceViewController: UIViewController, TableModelHost {
|
|||
let vc = destination as? ConfigurationViewController
|
||||
vc?.title = L10n.Service.Cells.Host.Parameters.caption
|
||||
vc?.initialConfiguration = uncheckedHostProfile.parameters.sessionConfiguration
|
||||
vc?.originalConfigurationURL = ProfileConfigurationFactory.shared.configurationURL(for: uncheckedHostProfile)
|
||||
vc?.originalConfigurationURL = service.configurationURL(for: uncheckedHostProfile)
|
||||
vc?.delegate = self
|
||||
|
||||
case .debugLogSegueIdentifier:
|
||||
|
@ -361,7 +361,8 @@ class ServiceViewController: UIViewController, TableModelHost {
|
|||
}
|
||||
|
||||
private func reportConnectivityIssue() {
|
||||
IssueReporter.shared.present(in: self)
|
||||
let attach = IssueReporter.Attachments(debugLog: true, profile: uncheckedProfile)
|
||||
IssueReporter.shared.present(in: self, withAttachments: attach)
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
0E1D72B4213C118500BA1586 /* ConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1D72B3213C118500BA1586 /* ConfigurationViewController.swift */; };
|
||||
0E2B494020FCFF990094784C /* Theme+Titles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2B493F20FCFF990094784C /* Theme+Titles.swift */; };
|
||||
0E2B494220FD16540094784C /* TransientStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2B494120FD16540094784C /* TransientStore.swift */; };
|
||||
0E2D11BA217DBEDE0096822C /* ProfileConfigurationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2D11B9217DBEDE0096822C /* ProfileConfigurationFactory.swift */; };
|
||||
0E2D11BA217DBEDE0096822C /* ConnectionService+Configurations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2D11B9217DBEDE0096822C /* ConnectionService+Configurations.swift */; };
|
||||
0E39BCF0214B9EF10035E9DE /* WebServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E39BCEF214B9EF10035E9DE /* WebServices.swift */; };
|
||||
0E39BCF3214DA9310035E9DE /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E39BCF2214DA9310035E9DE /* AppConstants.swift */; };
|
||||
0E3DA371215CB5BF00B40FC9 /* VersionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3DA370215CB5BF00B40FC9 /* VersionViewController.swift */; };
|
||||
|
@ -137,7 +137,7 @@
|
|||
0E1D72B3213C118500BA1586 /* ConfigurationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationViewController.swift; sourceTree = "<group>"; };
|
||||
0E2B493F20FCFF990094784C /* Theme+Titles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Titles.swift"; sourceTree = "<group>"; };
|
||||
0E2B494120FD16540094784C /* TransientStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransientStore.swift; sourceTree = "<group>"; };
|
||||
0E2D11B9217DBEDE0096822C /* ProfileConfigurationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileConfigurationFactory.swift; sourceTree = "<group>"; };
|
||||
0E2D11B9217DBEDE0096822C /* ConnectionService+Configurations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConnectionService+Configurations.swift"; sourceTree = "<group>"; };
|
||||
0E39BCEF214B9EF10035E9DE /* WebServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebServices.swift; sourceTree = "<group>"; };
|
||||
0E39BCF2214DA9310035E9DE /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = "<group>"; };
|
||||
0E3DA370215CB5BF00B40FC9 /* VersionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -382,12 +382,12 @@
|
|||
0EBE3AA2213DC1B000BFA2F5 /* Profiles */,
|
||||
0EBE3A9E213DC1A100BFA2F5 /* ConnectionProfile.swift */,
|
||||
0EBE3A9F213DC1A100BFA2F5 /* ConnectionService.swift */,
|
||||
0E2D11B9217DBEDE0096822C /* ConnectionService+Configurations.swift */,
|
||||
0EBBE8F42182361700106008 /* ConnectionService+Migration.swift */,
|
||||
0EDE8DE620C93945004C739C /* Credentials.swift */,
|
||||
0EC7F20420E24308004EA58E /* DebugLog.swift */,
|
||||
0ED38AE621404F100004D387 /* EndpointDataSource.swift */,
|
||||
0E89DFC4213DF7AE00741BA1 /* Preferences.swift */,
|
||||
0E2D11B9217DBEDE0096822C /* ProfileConfigurationFactory.swift */,
|
||||
0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */,
|
||||
0E2B494120FD16540094784C /* TransientStore.swift */,
|
||||
0E4C9CB820DB9BC600A0C59C /* TrustedNetworks.swift */,
|
||||
|
@ -842,7 +842,7 @@
|
|||
0ED31C1220CF0ABA0027975F /* Infrastructure.swift in Sources */,
|
||||
0EC7F20520E24308004EA58E /* DebugLog.swift in Sources */,
|
||||
0E4FD7E120D3E4C5002221FF /* MockVPNProvider.swift in Sources */,
|
||||
0E2D11BA217DBEDE0096822C /* ProfileConfigurationFactory.swift in Sources */,
|
||||
0E2D11BA217DBEDE0096822C /* ConnectionService+Configurations.swift in Sources */,
|
||||
0EBE3A90213C6F4000BFA2F5 /* TrustPolicy.swift in Sources */,
|
||||
0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */,
|
||||
0E89DFC8213E8FC500741BA1 /* SessionProxy+Communication.swift in Sources */,
|
||||
|
|
|
@ -51,9 +51,11 @@
|
|||
"wizards.host.cells.title_input.caption" = "Title";
|
||||
"wizards.host.cells.title_input.placeholder" = "My Profile";
|
||||
"wizards.host.sections.existing.header" = "Existing profiles";
|
||||
"wizards.host.alerts.existing" = "A host profile with the same title already exists. Replace it?";
|
||||
"wizards.host.alerts.unsupported" = "The configuration file contains an unsupported option (%@).";
|
||||
"wizards.host.alerts.parsing" = "Unable to parse the provided configuration file (%@).";
|
||||
"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 (%@).";
|
||||
"wizards.host.alerts.parsing.message" = "Unable to parse the provided configuration file (%@).";
|
||||
"wizards.host.alerts.buttons.report" = "Report an issue";
|
||||
|
||||
"service.welcome.message" = "Welcome to Passepartout!\n\nUse the organizer to add a new profile.";
|
||||
"service.sections.general.header" = "General";
|
||||
|
@ -171,11 +173,12 @@
|
|||
"vpn.errors.network" = "Network changed";
|
||||
|
||||
"issue_reporter.title" = "Report issue";
|
||||
"issue_reporter.message" = "The debug log of your latest connections is crucial to resolve your connectivity issues and is completely anonymous.";
|
||||
"issue_reporter.message" = "The debug log of your latest connections is crucial to resolve your connectivity issues and is completely anonymous.\n\nThe .ovpn configuration file, if any, is attached stripped of any sensitive data.\n\nPlease double check the email attachments if unsure.";
|
||||
"issue_reporter.buttons.accept" = "I understand";
|
||||
"issue_reporter.alerts.email_not_configured.message" = "No e-mail account is configured.";
|
||||
"issue_reporter.email.subject" = "%@ - Debug log";
|
||||
"issue_reporter.email.body" = "Hi,\n\ndescription of the issue:\n\n%@\n\nRegards";
|
||||
"issue_reporter.email.subject" = "%@ - Report issue";
|
||||
"issue_reporter.email.body" = "Hi,\n\n%@\n\n%@\n\nRegards";
|
||||
"issue_reporter.email.description" = "description of the issue:";
|
||||
|
||||
"about.title" = "About";
|
||||
"about.sections.info.header" = "General";
|
||||
|
|
|
@ -96,19 +96,12 @@ class AppConstants {
|
|||
|
||||
static var debugSnapshot: () -> String = { TransientStore.shared.service.vpnLog }
|
||||
|
||||
static var debugFilename: String {
|
||||
let fmt = DateFormatter()
|
||||
fmt.dateFormat = "yyyyMMdd-HHmmss"
|
||||
let iso = fmt.string(from: Date())
|
||||
return "debug-\(iso).txt"
|
||||
}
|
||||
|
||||
static let viewerRefreshInterval: TimeInterval = 3.0
|
||||
|
||||
static func configure() {
|
||||
let console = ConsoleDestination()
|
||||
console.useNSLog = true
|
||||
console.minLevel = .verbose
|
||||
console.minLevel = .debug
|
||||
SwiftyBeaver.addDestination(console)
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +109,24 @@ class AppConstants {
|
|||
class IssueReporter {
|
||||
static let recipient = "issues@\(Domain.name)"
|
||||
|
||||
static let attachmentMIME = "text/plain"
|
||||
class Filenames {
|
||||
static var debugLog: String {
|
||||
let fmt = DateFormatter()
|
||||
fmt.dateFormat = "yyyyMMdd-HHmmss"
|
||||
let iso = fmt.string(from: Date())
|
||||
return "debug-\(iso).txt"
|
||||
}
|
||||
|
||||
// static let configuration = "profile.ovpn"
|
||||
static let configuration = "profile.ovpn.txt"
|
||||
}
|
||||
|
||||
class MIME {
|
||||
static let debugLog = "text/plain"
|
||||
|
||||
// static let configuration = "application/x-openvpn-profile"
|
||||
static let configuration = "text/plain"
|
||||
}
|
||||
}
|
||||
|
||||
class URLs {
|
||||
|
|
|
@ -30,10 +30,8 @@ enum ApplicationError: Error {
|
|||
|
||||
case missingCredentials
|
||||
|
||||
case missingCA
|
||||
case missingConfiguration(option: String)
|
||||
|
||||
case emptyRemotes
|
||||
|
||||
case unsupportedConfiguration(option: String)
|
||||
|
||||
case migration
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// ConnectionService+Configurations.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/22/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
|
||||
|
||||
extension ConnectionService {
|
||||
func save(configurationURL: URL, for profile: ConnectionProfile) throws -> URL {
|
||||
let destinationURL = targetConfigurationURL(for: profile)
|
||||
let fm = FileManager.default
|
||||
try? fm.removeItem(at: destinationURL)
|
||||
try fm.copyItem(at: configurationURL, to: destinationURL)
|
||||
return destinationURL
|
||||
}
|
||||
|
||||
func configurationURL(for profile: ConnectionProfile) -> URL? {
|
||||
let url = targetConfigurationURL(for: profile)
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private func targetConfigurationURL(for profile: ConnectionProfile) -> URL {
|
||||
let contextURL = ConnectionService.ProfileKey(profile).contextURL(in: self)
|
||||
return contextURL.appendingPathComponent("\(profile.id).ovpn")
|
||||
}
|
||||
}
|
|
@ -64,19 +64,21 @@ class ConnectionService: Codable {
|
|||
id = profile.id
|
||||
}
|
||||
|
||||
fileprivate func profileURL(in service: ConnectionService) -> URL {
|
||||
let contextURL: URL
|
||||
func contextURL(in service: ConnectionService) -> URL {
|
||||
switch context {
|
||||
case .provider:
|
||||
contextURL = service.providersURL
|
||||
return service.providersURL
|
||||
|
||||
case .host:
|
||||
contextURL = service.hostsURL
|
||||
return service.hostsURL
|
||||
}
|
||||
return ConnectionService.url(in: contextURL, forProfileId: id)
|
||||
}
|
||||
|
||||
func profileURL(in service: ConnectionService) -> URL {
|
||||
return ConnectionService.url(in: contextURL(in: service), forProfileId: id)
|
||||
}
|
||||
|
||||
fileprivate func profileData(in service: ConnectionService) throws -> Data {
|
||||
func profileData(in service: ConnectionService) throws -> Data {
|
||||
return try Data(contentsOf: profileURL(in: service))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
//
|
||||
// ProfileConfigurationFactory.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 10/22/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
|
||||
|
||||
protocol ProfileConfigurationSource {
|
||||
var id: String { get }
|
||||
|
||||
var profileDirectory: String { get }
|
||||
}
|
||||
|
||||
extension ProfileConfigurationSource {
|
||||
var profileConfigurationPath: String {
|
||||
return "\(profileDirectory)/\(id).ovpn"
|
||||
}
|
||||
}
|
||||
|
||||
extension ProviderConnectionProfile: ProfileConfigurationSource {
|
||||
var profileDirectory: String {
|
||||
return AppConstants.Store.providersDirectory
|
||||
}
|
||||
}
|
||||
|
||||
extension HostConnectionProfile: ProfileConfigurationSource {
|
||||
var profileDirectory: String {
|
||||
return AppConstants.Store.hostsDirectory
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileConfigurationFactory {
|
||||
static let shared = ProfileConfigurationFactory()
|
||||
|
||||
private let configurationsPath: URL
|
||||
|
||||
private init() {
|
||||
let fm = FileManager.default
|
||||
configurationsPath = fm.userURL(for: .documentDirectory, appending: nil)
|
||||
try? fm.createDirectory(at: configurationsPath, withIntermediateDirectories: false, attributes: nil)
|
||||
}
|
||||
|
||||
func save(url: URL, for profile: ProfileConfigurationSource) throws -> URL {
|
||||
let savedUrl = targetConfigurationURL(for: profile)
|
||||
let fm = FileManager.default
|
||||
try? fm.removeItem(at: savedUrl)
|
||||
try fm.copyItem(at: url, to: savedUrl)
|
||||
return savedUrl
|
||||
}
|
||||
|
||||
func configurationURL(for profile: ProfileConfigurationSource) -> URL? {
|
||||
let url = targetConfigurationURL(for: profile)
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private func targetConfigurationURL(for profile: ProfileConfigurationSource) -> URL {
|
||||
return configurationsPath.appendingPathComponent(profile.profileConfigurationPath)
|
||||
}
|
||||
}
|
|
@ -301,7 +301,7 @@ internal enum L10n {
|
|||
}
|
||||
|
||||
internal enum IssueReporter {
|
||||
/// The debug log of your latest connections is crucial to resolve your connectivity issues and is completely anonymous.
|
||||
/// The debug log of your latest connections is crucial to resolve your connectivity issues and is completely anonymous.\n\nThe .ovpn configuration file, if any, is attached stripped of any sensitive data.\n\nPlease double check the email attachments if unsure.
|
||||
internal static let message = L10n.tr("Localizable", "issue_reporter.message")
|
||||
/// Report issue
|
||||
internal static let title = L10n.tr("Localizable", "issue_reporter.title")
|
||||
|
@ -320,11 +320,13 @@ internal enum L10n {
|
|||
}
|
||||
|
||||
internal enum Email {
|
||||
/// Hi,\n\ndescription of the issue:\n\n%@\n\nRegards
|
||||
internal static func body(_ p1: String) -> String {
|
||||
return L10n.tr("Localizable", "issue_reporter.email.body", p1)
|
||||
/// Hi,\n\n%@\n\n%@\n\nRegards
|
||||
internal static func body(_ p1: String, _ p2: String) -> String {
|
||||
return L10n.tr("Localizable", "issue_reporter.email.body", p1, p2)
|
||||
}
|
||||
/// %@ - Debug log
|
||||
/// description of the issue:
|
||||
internal static let description = L10n.tr("Localizable", "issue_reporter.email.description")
|
||||
/// %@ - Report issue
|
||||
internal static func subject(_ p1: String) -> String {
|
||||
return L10n.tr("Localizable", "issue_reporter.email.subject", p1)
|
||||
}
|
||||
|
@ -731,15 +733,36 @@ internal enum L10n {
|
|||
internal enum Host {
|
||||
|
||||
internal enum Alerts {
|
||||
/// A host profile with the same title already exists. Replace it?
|
||||
internal static let existing = L10n.tr("Localizable", "wizards.host.alerts.existing")
|
||||
/// Unable to parse the provided configuration file (%@).
|
||||
internal static func parsing(_ p1: String) -> String {
|
||||
return L10n.tr("Localizable", "wizards.host.alerts.parsing", p1)
|
||||
|
||||
internal enum Buttons {
|
||||
/// Report an issue
|
||||
internal static let report = L10n.tr("Localizable", "wizards.host.alerts.buttons.report")
|
||||
}
|
||||
/// The configuration file contains an unsupported option (%@).
|
||||
internal static func unsupported(_ p1: String) -> String {
|
||||
return L10n.tr("Localizable", "wizards.host.alerts.unsupported", p1)
|
||||
|
||||
internal enum Existing {
|
||||
/// A host profile with the same title already exists. Replace it?
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ extension TunnelKitProvider.Configuration {
|
|||
static let blockEnd = Utils.regex("^<\\/[\\w\\-]+>")
|
||||
}
|
||||
|
||||
static func parsed(from url: URL) throws -> (String, TunnelKitProvider.Configuration) {
|
||||
static func parsed(from url: URL, stripped: UnsafeMutablePointer<[String]>? = nil) throws -> (String, TunnelKitProvider.Configuration) {
|
||||
let lines = try String(contentsOf: url).trimmedLines()
|
||||
|
||||
var defaultProto: TunnelKitProvider.SocketType?
|
||||
|
@ -90,7 +90,16 @@ extension TunnelKitProvider.Configuration {
|
|||
for line in lines {
|
||||
log.verbose(line)
|
||||
|
||||
var isHandled = false
|
||||
var strippedLine = line
|
||||
defer {
|
||||
if isHandled {
|
||||
stripped?.pointee.append(strippedLine)
|
||||
}
|
||||
}
|
||||
|
||||
Regex.blockBegin.enumerateComponents(in: line) {
|
||||
isHandled = true
|
||||
let tag = $0.first!
|
||||
let from = tag.index(after: tag.startIndex)
|
||||
let to = tag.index(before: tag.endIndex)
|
||||
|
@ -99,6 +108,7 @@ extension TunnelKitProvider.Configuration {
|
|||
currentBlock = []
|
||||
}
|
||||
Regex.blockEnd.enumerateComponents(in: line) {
|
||||
isHandled = true
|
||||
let tag = $0.first!
|
||||
let from = tag.index(tag.startIndex, offsetBy: 2)
|
||||
let to = tag.index(before: tag.endIndex)
|
||||
|
@ -138,8 +148,9 @@ extension TunnelKitProvider.Configuration {
|
|||
currentBlock.append(line)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Regex.proto.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let str = $0.first else {
|
||||
return
|
||||
}
|
||||
|
@ -149,26 +160,35 @@ extension TunnelKitProvider.Configuration {
|
|||
}
|
||||
}
|
||||
Regex.port.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let str = $0.first else {
|
||||
return
|
||||
}
|
||||
defaultPort = UInt16(str)
|
||||
}
|
||||
Regex.remote.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let hostname = $0.first else {
|
||||
return
|
||||
}
|
||||
var port: UInt16?
|
||||
var proto: TunnelKitProvider.SocketType?
|
||||
var strippedComponents = ["remote", "<hostname>"]
|
||||
if $0.count > 1 {
|
||||
port = UInt16($0[1])
|
||||
strippedComponents.append($0[1])
|
||||
}
|
||||
if $0.count > 2 {
|
||||
proto = TunnelKitProvider.SocketType(protoString: $0[2])
|
||||
strippedComponents.append($0[2])
|
||||
}
|
||||
remotes.append((hostname, port, proto))
|
||||
|
||||
// replace private data
|
||||
strippedLine = strippedComponents.joined(separator: " ")
|
||||
}
|
||||
Regex.cipher.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let rawValue = $0.first else {
|
||||
return
|
||||
}
|
||||
|
@ -178,6 +198,7 @@ extension TunnelKitProvider.Configuration {
|
|||
}
|
||||
}
|
||||
Regex.auth.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let rawValue = $0.first else {
|
||||
return
|
||||
}
|
||||
|
@ -187,24 +208,29 @@ extension TunnelKitProvider.Configuration {
|
|||
}
|
||||
}
|
||||
Regex.compLZO.enumerateComponents(in: line) { _ in
|
||||
isHandled = true
|
||||
compressionFraming = .compLZO
|
||||
}
|
||||
Regex.compress.enumerateComponents(in: line) { _ in
|
||||
isHandled = true
|
||||
compressionFraming = .compress
|
||||
}
|
||||
Regex.keyDirection.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first, let value = Int(arg) else {
|
||||
return
|
||||
}
|
||||
keyDirection = StaticKey.Direction(rawValue: value)
|
||||
}
|
||||
Regex.ping.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first else {
|
||||
return
|
||||
}
|
||||
keepAliveSeconds = TimeInterval(arg)
|
||||
}
|
||||
Regex.renegSec.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let arg = $0.first else {
|
||||
return
|
||||
}
|
||||
|
@ -219,6 +245,9 @@ extension TunnelKitProvider.Configuration {
|
|||
Regex.externalFiles.enumerateArguments(in: line) { (_) in
|
||||
unsupportedError = ApplicationError.unsupportedConfiguration(option: "external file: \"\(line)\"")
|
||||
}
|
||||
if line.contains("mtu") || line.contains("mssfix") {
|
||||
isHandled = true
|
||||
}
|
||||
|
||||
if let error = unsupportedError {
|
||||
throw error
|
||||
|
@ -226,13 +255,13 @@ extension TunnelKitProvider.Configuration {
|
|||
}
|
||||
|
||||
guard let ca = optCA else {
|
||||
throw ApplicationError.missingCA
|
||||
throw ApplicationError.missingConfiguration(option: "ca")
|
||||
}
|
||||
|
||||
// XXX: only reads first remote
|
||||
// hostnames = remotes.map { $0.0 }
|
||||
guard !remotes.isEmpty else {
|
||||
throw ApplicationError.emptyRemotes
|
||||
throw ApplicationError.missingConfiguration(option: "remote")
|
||||
}
|
||||
let hostname = remotes[0].0
|
||||
|
||||
|
|
|
@ -44,6 +44,13 @@ class FileConfigurationTests: XCTestCase {
|
|||
XCTAssertEqual(cfg.sessionConfiguration.digest, .sha1)
|
||||
}
|
||||
|
||||
func testStripped() throws {
|
||||
var lines: [String] = []
|
||||
_ = try TunnelKitProvider.Configuration.parsed(from: url(withName: "pia-hungary"), stripped: &lines)
|
||||
let cfg = lines.joined(separator: "\n")
|
||||
print(cfg)
|
||||
}
|
||||
|
||||
private func url(withName name: String) -> URL {
|
||||
return Bundle(for: FileConfigurationTests.self).url(forResource: name, withExtension: "ovpn")!
|
||||
}
|
||||
|
|
|
@ -70,14 +70,12 @@ Passepartout can import .ovpn configuration files. This way you can fine-tune en
|
|||
Unsupported:
|
||||
|
||||
- UDP fragmentation, i.e. `--fragment`
|
||||
|
||||
Unsupported (probably ever):
|
||||
|
||||
- Compression
|
||||
- `--comp-lzo` other than `no`
|
||||
- `--compress` other than empty
|
||||
- Proxy
|
||||
- External file references (inline `<block>` only)
|
||||
- Encrypted certificate private key (will raise error TunnelKitNative Code=205)
|
||||
|
||||
Ignored:
|
||||
|
||||
|
|
Loading…
Reference in New Issue