Merge pull request #13 from keeshux/attach-ovpn-to-report

Attach .ovpn to connectivity issue report
This commit is contained in:
Davide De Rosa 2018-10-27 09:46:00 +02:00 committed by GitHub
commit 02c8e7b6ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 241 additions and 152 deletions

View File

@ -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/), 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). 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) ## 1.0 beta 1107 (2018-10-26)
### Changed ### Changed

View File

@ -118,17 +118,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
} }
nav.modalPresentationStyle = .formSheet nav.modalPresentationStyle = .formSheet
root.present(nav, animated: true, completion: nil) 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) { } catch ApplicationError.unsupportedConfiguration(let option) {
let alert = Macros.alert(L10n.Organizer.Sections.Hosts.header, L10n.Wizards.Host.Alerts.unsupported(option)) let message = L10n.Wizards.Host.Alerts.Unsupported.message(option)
alert.addCancelAction(L10n.Global.ok) alertConfigurationImportError(url: url, in: root, withMessage: message)
root.present(alert, animated: true, completion: nil)
} catch let e { } catch let e {
let alert = Macros.alert(L10n.Organizer.Sections.Hosts.header, L10n.Wizards.Host.Alerts.parsing(e.localizedDescription)) let message = L10n.Wizards.Host.Alerts.Parsing.message(e.localizedDescription)
alert.addCancelAction(L10n.Global.ok) alertConfigurationImportError(url: url, in: root, withMessage: message)
root.present(alert, 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 {

View File

@ -24,9 +24,28 @@
// //
import Foundation import Foundation
import TunnelKit
import MessageUI import MessageUI
class IssueReporter: NSObject { 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() static let shared = IssueReporter()
private weak var viewController: UIViewController? private weak var viewController: UIViewController?
@ -35,7 +54,7 @@ class IssueReporter: NSObject {
super.init() super.init()
} }
func present(in viewController: UIViewController) { func present(in viewController: UIViewController, withAttachments attachments: Attachments) {
guard MFMailComposeViewController.canSendMail() else { guard MFMailComposeViewController.canSendMail() else {
let alert = Macros.alert(L10n.IssueReporter.title, L10n.IssueReporter.Alerts.EmailNotConfigured.message) let alert = Macros.alert(L10n.IssueReporter.title, L10n.IssueReporter.Alerts.EmailNotConfigured.message)
alert.addCancelAction(L10n.Global.ok) alert.addCancelAction(L10n.Global.ok)
@ -45,26 +64,40 @@ class IssueReporter: NSObject {
self.viewController = viewController self.viewController = viewController
let alert = Macros.alert(L10n.IssueReporter.title, L10n.IssueReporter.message) if attachments.debugLog {
alert.addDefaultAction(L10n.IssueReporter.Buttons.accept) { let alert = Macros.alert(L10n.IssueReporter.title, L10n.IssueReporter.message)
VPN.shared.requestDebugLog(fallback: AppConstants.Log.debugSnapshot) { alert.addDefaultAction(L10n.IssueReporter.Buttons.accept) {
self.composeEmail(withDebugLog: $0) 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 metadata = DebugLog(raw: "--").decoratedString()
let vc = MFMailComposeViewController() let vc = MFMailComposeViewController()
vc.setToRecipients([AppConstants.IssueReporter.recipient]) vc.setToRecipients([AppConstants.IssueReporter.recipient])
vc.setSubject(L10n.IssueReporter.Email.subject(GroupConstants.App.name)) 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 { if let raw = debugLog {
let attachment = DebugLog(raw: raw).decoratedData() 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.mailComposeDelegate = self
vc.apply(Theme.current) vc.apply(Theme.current)

View File

@ -64,7 +64,7 @@ class DebugLogViewController: UIViewController {
} }
let data = DebugLog(raw: raw).decoratedData() 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) let url = URL(fileURLWithPath: path)
do { do {
try data.write(to: url) try data.write(to: url)

View File

@ -134,7 +134,7 @@ class WizardHostViewController: UITableViewController, TableModelHost, Wizard {
profile.parameters = file.configuration profile.parameters = file.configuration
guard !TransientStore.shared.service.containsProfile(profile) else { 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) { alert.addDefaultAction(L10n.Global.ok) {
self.next(withProfile: profile) self.next(withProfile: profile)
} }
@ -159,8 +159,8 @@ class WizardHostViewController: UITableViewController, TableModelHost, Wizard {
} }
if let url = parsedFile?.url { if let url = parsedFile?.url {
do { do {
let savedUrl = try ProfileConfigurationFactory.shared.save(url: url, for: profile) let savedURL = try TransientStore.shared.service.save(configurationURL: url, for: profile)
log.debug("Associated .ovpn configuration file to profile '\(profile.id)': \(savedUrl)") log.debug("Associated .ovpn configuration file to profile '\(profile.id)': \(savedURL)")
} catch let e { } catch let e {
log.error("Could not associate .ovpn configuration file to profile: \(e)") log.error("Could not associate .ovpn configuration file to profile: \(e)")
} }

View File

@ -154,7 +154,7 @@ class ServiceViewController: UIViewController, TableModelHost {
let vc = destination as? ConfigurationViewController let vc = destination as? ConfigurationViewController
vc?.title = L10n.Service.Cells.Host.Parameters.caption vc?.title = L10n.Service.Cells.Host.Parameters.caption
vc?.initialConfiguration = uncheckedHostProfile.parameters.sessionConfiguration vc?.initialConfiguration = uncheckedHostProfile.parameters.sessionConfiguration
vc?.originalConfigurationURL = ProfileConfigurationFactory.shared.configurationURL(for: uncheckedHostProfile) vc?.originalConfigurationURL = service.configurationURL(for: uncheckedHostProfile)
vc?.delegate = self vc?.delegate = self
case .debugLogSegueIdentifier: case .debugLogSegueIdentifier:
@ -361,7 +361,8 @@ class ServiceViewController: UIViewController, TableModelHost {
} }
private func reportConnectivityIssue() { 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 // MARK: Notifications

View File

@ -20,7 +20,7 @@
0E1D72B4213C118500BA1586 /* ConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1D72B3213C118500BA1586 /* ConfigurationViewController.swift */; }; 0E1D72B4213C118500BA1586 /* ConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1D72B3213C118500BA1586 /* ConfigurationViewController.swift */; };
0E2B494020FCFF990094784C /* Theme+Titles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2B493F20FCFF990094784C /* Theme+Titles.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 */; }; 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 */; }; 0E39BCF0214B9EF10035E9DE /* WebServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E39BCEF214B9EF10035E9DE /* WebServices.swift */; };
0E39BCF3214DA9310035E9DE /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E39BCF2214DA9310035E9DE /* AppConstants.swift */; }; 0E39BCF3214DA9310035E9DE /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E39BCF2214DA9310035E9DE /* AppConstants.swift */; };
0E3DA371215CB5BF00B40FC9 /* VersionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3DA370215CB5BF00B40FC9 /* VersionViewController.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 0E3DA370215CB5BF00B40FC9 /* VersionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionViewController.swift; sourceTree = "<group>"; };
@ -382,12 +382,12 @@
0EBE3AA2213DC1B000BFA2F5 /* Profiles */, 0EBE3AA2213DC1B000BFA2F5 /* Profiles */,
0EBE3A9E213DC1A100BFA2F5 /* ConnectionProfile.swift */, 0EBE3A9E213DC1A100BFA2F5 /* ConnectionProfile.swift */,
0EBE3A9F213DC1A100BFA2F5 /* ConnectionService.swift */, 0EBE3A9F213DC1A100BFA2F5 /* ConnectionService.swift */,
0E2D11B9217DBEDE0096822C /* ConnectionService+Configurations.swift */,
0EBBE8F42182361700106008 /* ConnectionService+Migration.swift */, 0EBBE8F42182361700106008 /* ConnectionService+Migration.swift */,
0EDE8DE620C93945004C739C /* Credentials.swift */, 0EDE8DE620C93945004C739C /* Credentials.swift */,
0EC7F20420E24308004EA58E /* DebugLog.swift */, 0EC7F20420E24308004EA58E /* DebugLog.swift */,
0ED38AE621404F100004D387 /* EndpointDataSource.swift */, 0ED38AE621404F100004D387 /* EndpointDataSource.swift */,
0E89DFC4213DF7AE00741BA1 /* Preferences.swift */, 0E89DFC4213DF7AE00741BA1 /* Preferences.swift */,
0E2D11B9217DBEDE0096822C /* ProfileConfigurationFactory.swift */,
0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */, 0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */,
0E2B494120FD16540094784C /* TransientStore.swift */, 0E2B494120FD16540094784C /* TransientStore.swift */,
0E4C9CB820DB9BC600A0C59C /* TrustedNetworks.swift */, 0E4C9CB820DB9BC600A0C59C /* TrustedNetworks.swift */,
@ -842,7 +842,7 @@
0ED31C1220CF0ABA0027975F /* Infrastructure.swift in Sources */, 0ED31C1220CF0ABA0027975F /* Infrastructure.swift in Sources */,
0EC7F20520E24308004EA58E /* DebugLog.swift in Sources */, 0EC7F20520E24308004EA58E /* DebugLog.swift in Sources */,
0E4FD7E120D3E4C5002221FF /* MockVPNProvider.swift in Sources */, 0E4FD7E120D3E4C5002221FF /* MockVPNProvider.swift in Sources */,
0E2D11BA217DBEDE0096822C /* ProfileConfigurationFactory.swift in Sources */, 0E2D11BA217DBEDE0096822C /* ConnectionService+Configurations.swift in Sources */,
0EBE3A90213C6F4000BFA2F5 /* TrustPolicy.swift in Sources */, 0EBE3A90213C6F4000BFA2F5 /* TrustPolicy.swift in Sources */,
0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */, 0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */,
0E89DFC8213E8FC500741BA1 /* SessionProxy+Communication.swift in Sources */, 0E89DFC8213E8FC500741BA1 /* SessionProxy+Communication.swift in Sources */,

View File

@ -51,9 +51,11 @@
"wizards.host.cells.title_input.caption" = "Title"; "wizards.host.cells.title_input.caption" = "Title";
"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" = "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.unsupported" = "The configuration file contains an unsupported option (%@)."; "wizards.host.alerts.missing.message" = "The configuration file lacks a required option (%@).";
"wizards.host.alerts.parsing" = "Unable to parse the provided configuration file (%@)."; "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.welcome.message" = "Welcome to Passepartout!\n\nUse the organizer to add a new profile.";
"service.sections.general.header" = "General"; "service.sections.general.header" = "General";
@ -171,11 +173,12 @@
"vpn.errors.network" = "Network changed"; "vpn.errors.network" = "Network changed";
"issue_reporter.title" = "Report issue"; "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.buttons.accept" = "I understand";
"issue_reporter.alerts.email_not_configured.message" = "No e-mail account is configured."; "issue_reporter.alerts.email_not_configured.message" = "No e-mail account is configured.";
"issue_reporter.email.subject" = "%@ - Debug log"; "issue_reporter.email.subject" = "%@ - Report issue";
"issue_reporter.email.body" = "Hi,\n\ndescription of the issue:\n\n%@\n\nRegards"; "issue_reporter.email.body" = "Hi,\n\n%@\n\n%@\n\nRegards";
"issue_reporter.email.description" = "description of the issue:";
"about.title" = "About"; "about.title" = "About";
"about.sections.info.header" = "General"; "about.sections.info.header" = "General";

View File

@ -96,19 +96,12 @@ class AppConstants {
static var debugSnapshot: () -> String = { TransientStore.shared.service.vpnLog } 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 let viewerRefreshInterval: TimeInterval = 3.0
static func configure() { static func configure() {
let console = ConsoleDestination() let console = ConsoleDestination()
console.useNSLog = true console.useNSLog = true
console.minLevel = .verbose console.minLevel = .debug
SwiftyBeaver.addDestination(console) SwiftyBeaver.addDestination(console)
} }
} }
@ -116,7 +109,24 @@ class AppConstants {
class IssueReporter { class IssueReporter {
static let recipient = "issues@\(Domain.name)" 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 { class URLs {

View File

@ -30,10 +30,8 @@ enum ApplicationError: Error {
case missingCredentials case missingCredentials
case missingCA case missingConfiguration(option: String)
case emptyRemotes
case unsupportedConfiguration(option: String) case unsupportedConfiguration(option: String)
case migration case migration

View File

@ -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")
}
}

View File

@ -64,19 +64,21 @@ class ConnectionService: Codable {
id = profile.id id = profile.id
} }
fileprivate func profileURL(in service: ConnectionService) -> URL { func contextURL(in service: ConnectionService) -> URL {
let contextURL: URL
switch context { switch context {
case .provider: case .provider:
contextURL = service.providersURL return service.providersURL
case .host: 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)) return try Data(contentsOf: profileURL(in: service))
} }

View File

@ -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)
}
}

View File

@ -301,7 +301,7 @@ internal enum L10n {
} }
internal enum IssueReporter { 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") internal static let message = L10n.tr("Localizable", "issue_reporter.message")
/// Report issue /// Report issue
internal static let title = L10n.tr("Localizable", "issue_reporter.title") internal static let title = L10n.tr("Localizable", "issue_reporter.title")
@ -320,11 +320,13 @@ internal enum L10n {
} }
internal enum Email { internal enum Email {
/// Hi,\n\ndescription of the issue:\n\n%@\n\nRegards /// Hi,\n\n%@\n\n%@\n\nRegards
internal static func body(_ p1: String) -> String { internal static func body(_ p1: String, _ p2: String) -> String {
return L10n.tr("Localizable", "issue_reporter.email.body", p1) 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 { internal static func subject(_ p1: String) -> String {
return L10n.tr("Localizable", "issue_reporter.email.subject", p1) return L10n.tr("Localizable", "issue_reporter.email.subject", p1)
} }
@ -731,15 +733,36 @@ internal enum L10n {
internal enum Host { internal enum Host {
internal enum Alerts { 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") internal enum Buttons {
/// Unable to parse the provided configuration file (%@). /// Report an issue
internal static func parsing(_ p1: String) -> String { internal static let report = L10n.tr("Localizable", "wizards.host.alerts.buttons.report")
return L10n.tr("Localizable", "wizards.host.alerts.parsing", p1)
} }
/// The configuration file contains an unsupported option (%@).
internal static func unsupported(_ p1: String) -> String { internal enum Existing {
return L10n.tr("Localizable", "wizards.host.alerts.unsupported", p1) /// 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)
}
} }
} }

View File

@ -62,7 +62,7 @@ extension TunnelKitProvider.Configuration {
static let blockEnd = Utils.regex("^<\\/[\\w\\-]+>") 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() let lines = try String(contentsOf: url).trimmedLines()
var defaultProto: TunnelKitProvider.SocketType? var defaultProto: TunnelKitProvider.SocketType?
@ -90,7 +90,16 @@ extension TunnelKitProvider.Configuration {
for line in lines { for line in lines {
log.verbose(line) log.verbose(line)
var isHandled = false
var strippedLine = line
defer {
if isHandled {
stripped?.pointee.append(strippedLine)
}
}
Regex.blockBegin.enumerateComponents(in: line) { Regex.blockBegin.enumerateComponents(in: line) {
isHandled = true
let tag = $0.first! let tag = $0.first!
let from = tag.index(after: tag.startIndex) let from = tag.index(after: tag.startIndex)
let to = tag.index(before: tag.endIndex) let to = tag.index(before: tag.endIndex)
@ -99,6 +108,7 @@ extension TunnelKitProvider.Configuration {
currentBlock = [] currentBlock = []
} }
Regex.blockEnd.enumerateComponents(in: line) { Regex.blockEnd.enumerateComponents(in: line) {
isHandled = true
let tag = $0.first! let tag = $0.first!
let from = tag.index(tag.startIndex, offsetBy: 2) let from = tag.index(tag.startIndex, offsetBy: 2)
let to = tag.index(before: tag.endIndex) let to = tag.index(before: tag.endIndex)
@ -138,8 +148,9 @@ extension TunnelKitProvider.Configuration {
currentBlock.append(line) currentBlock.append(line)
continue continue
} }
Regex.proto.enumerateArguments(in: line) { Regex.proto.enumerateArguments(in: line) {
isHandled = true
guard let str = $0.first else { guard let str = $0.first else {
return return
} }
@ -149,26 +160,35 @@ extension TunnelKitProvider.Configuration {
} }
} }
Regex.port.enumerateArguments(in: line) { Regex.port.enumerateArguments(in: line) {
isHandled = true
guard let str = $0.first else { guard let str = $0.first else {
return return
} }
defaultPort = UInt16(str) defaultPort = UInt16(str)
} }
Regex.remote.enumerateArguments(in: line) { Regex.remote.enumerateArguments(in: line) {
isHandled = true
guard let hostname = $0.first else { guard let hostname = $0.first else {
return return
} }
var port: UInt16? var port: UInt16?
var proto: TunnelKitProvider.SocketType? var proto: TunnelKitProvider.SocketType?
var strippedComponents = ["remote", "<hostname>"]
if $0.count > 1 { if $0.count > 1 {
port = UInt16($0[1]) port = UInt16($0[1])
strippedComponents.append($0[1])
} }
if $0.count > 2 { if $0.count > 2 {
proto = TunnelKitProvider.SocketType(protoString: $0[2]) proto = TunnelKitProvider.SocketType(protoString: $0[2])
strippedComponents.append($0[2])
} }
remotes.append((hostname, port, proto)) remotes.append((hostname, port, proto))
// replace private data
strippedLine = strippedComponents.joined(separator: " ")
} }
Regex.cipher.enumerateArguments(in: line) { Regex.cipher.enumerateArguments(in: line) {
isHandled = true
guard let rawValue = $0.first else { guard let rawValue = $0.first else {
return return
} }
@ -178,6 +198,7 @@ extension TunnelKitProvider.Configuration {
} }
} }
Regex.auth.enumerateArguments(in: line) { Regex.auth.enumerateArguments(in: line) {
isHandled = true
guard let rawValue = $0.first else { guard let rawValue = $0.first else {
return return
} }
@ -187,24 +208,29 @@ extension TunnelKitProvider.Configuration {
} }
} }
Regex.compLZO.enumerateComponents(in: line) { _ in Regex.compLZO.enumerateComponents(in: line) { _ in
isHandled = true
compressionFraming = .compLZO compressionFraming = .compLZO
} }
Regex.compress.enumerateComponents(in: line) { _ in Regex.compress.enumerateComponents(in: line) { _ in
isHandled = true
compressionFraming = .compress compressionFraming = .compress
} }
Regex.keyDirection.enumerateArguments(in: line) { Regex.keyDirection.enumerateArguments(in: line) {
isHandled = true
guard let arg = $0.first, let value = Int(arg) else { guard let arg = $0.first, let value = Int(arg) else {
return return
} }
keyDirection = StaticKey.Direction(rawValue: value) keyDirection = StaticKey.Direction(rawValue: value)
} }
Regex.ping.enumerateArguments(in: line) { Regex.ping.enumerateArguments(in: line) {
isHandled = true
guard let arg = $0.first else { guard let arg = $0.first else {
return return
} }
keepAliveSeconds = TimeInterval(arg) keepAliveSeconds = TimeInterval(arg)
} }
Regex.renegSec.enumerateArguments(in: line) { Regex.renegSec.enumerateArguments(in: line) {
isHandled = true
guard let arg = $0.first else { guard let arg = $0.first else {
return return
} }
@ -219,6 +245,9 @@ extension TunnelKitProvider.Configuration {
Regex.externalFiles.enumerateArguments(in: line) { (_) in Regex.externalFiles.enumerateArguments(in: line) { (_) in
unsupportedError = ApplicationError.unsupportedConfiguration(option: "external file: \"\(line)\"") unsupportedError = ApplicationError.unsupportedConfiguration(option: "external file: \"\(line)\"")
} }
if line.contains("mtu") || line.contains("mssfix") {
isHandled = true
}
if let error = unsupportedError { if let error = unsupportedError {
throw error throw error
@ -226,13 +255,13 @@ extension TunnelKitProvider.Configuration {
} }
guard let ca = optCA else { guard let ca = optCA else {
throw ApplicationError.missingCA throw ApplicationError.missingConfiguration(option: "ca")
} }
// XXX: only reads first remote // XXX: only reads first remote
// hostnames = remotes.map { $0.0 } // hostnames = remotes.map { $0.0 }
guard !remotes.isEmpty else { guard !remotes.isEmpty else {
throw ApplicationError.emptyRemotes throw ApplicationError.missingConfiguration(option: "remote")
} }
let hostname = remotes[0].0 let hostname = remotes[0].0

View File

@ -44,6 +44,13 @@ class FileConfigurationTests: XCTestCase {
XCTAssertEqual(cfg.sessionConfiguration.digest, .sha1) 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 { private func url(withName name: String) -> URL {
return Bundle(for: FileConfigurationTests.self).url(forResource: name, withExtension: "ovpn")! return Bundle(for: FileConfigurationTests.self).url(forResource: name, withExtension: "ovpn")!
} }

View File

@ -70,14 +70,12 @@ Passepartout can import .ovpn configuration files. This way you can fine-tune en
Unsupported: Unsupported:
- UDP fragmentation, i.e. `--fragment` - UDP fragmentation, i.e. `--fragment`
Unsupported (probably ever):
- Compression - Compression
- `--comp-lzo` other than `no` - `--comp-lzo` other than `no`
- `--compress` other than empty - `--compress` other than empty
- Proxy - Proxy
- External file references (inline `<block>` only) - External file references (inline `<block>` only)
- Encrypted certificate private key (will raise error TunnelKitNative Code=205)
Ignored: Ignored: