passepartout-apple/Passepartout/App/macOS/Global/HostImporter.swift

239 lines
9.2 KiB
Swift

//
// HostImporter.swift
// Passepartout
//
// Created by Davide De Rosa on 8/18/19.
// Copyright (c) 2021 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Cocoa
import PassepartoutCore
import SwiftyBeaver
private let log = SwiftyBeaver.self
class HostImporter {
private let service = TransientStore.shared.service
private let windowController: NSWindowController?
private let viewController: NSViewController?
private weak var accountDelegate: AccountViewControllerDelegate?
private let configurationURL: URL
private var createdTitle: String?
private var replacedProfile: ConnectionProfile?
init(withConfigurationURL configurationURL: URL) {
self.configurationURL = configurationURL
log.debug("Parsing configuration URL: \(configurationURL)")
windowController = WindowManager.shared.showOrganizer()
viewController = windowController?.contentViewController
accountDelegate = viewController as? AccountViewControllerDelegate
}
func importHost(withPassphrase passphrase: String?) {
let result: OpenVPN.ConfigurationParser.Result
do {
result = try OpenVPN.ConfigurationParser.parsed(fromURL: configurationURL, passphrase: passphrase)
} catch let e as ConfigurationError {
switch e {
case .encryptionPassphrase, .unableToDecrypt(_):
enterPassphraseForHost(at: configurationURL)
default:
let message = HostImporter.localizedMessage(forError: e)
let alert = Macros.warning(configurationURL.normalizedFilename, message)
_ = alert.presentModally(withOK: L10n.Global.ok, cancel: nil)
}
return
} catch let e {
let message = HostImporter.localizedMessage(forError: e)
let alert = Macros.warning(configurationURL.normalizedFilename, message)
_ = alert.presentModally(withOK: L10n.Global.ok, cancel: nil)
return
}
if let warning = result.warning {
let message = HostImporter.localizedDetailsMessage(forWarning: warning)
let alert = Macros.warning(configurationURL.normalizedFilename, L10n.ParsedFile.Alerts.PotentiallyUnsupported.message(message))
if alert.presentModally(withOK: L10n.Global.ok, cancel: L10n.Global.cancel) {
enterProfileName(forHostWithResult: result)
}
return
}
enterProfileName(forHostWithResult: result)
}
private func enterPassphraseForHost(at url: URL) {
let vc = StoryboardScene.Main.textInputViewController.instantiate()
vc.caption = L10n.ParsedFile.Alerts.EncryptionPassphrase.message
vc.isSecure = true
vc.object = url
vc.delegate = self
present(vc)
}
private func enterProfileName(forHostWithResult result: OpenVPN.ConfigurationParser.Result) {
guard let title = result.url?.normalizedFilename, let hostname = result.configuration.hostname else {
return
}
let vc = StoryboardScene.Main.textInputViewController.instantiate()
vc.caption = L10n.Service.Alerts.Rename.title.asCaption
let profile = HostConnectionProfile(hostname: hostname)
let builder = OpenVPNProvider.ConfigurationBuilder(sessionConfiguration: result.configuration)
profile.parameters = builder.build()
vc.text = title
vc.placeholder = L10n.Global.Host.TitleInput.placeholder
vc.object = profile
vc.delegate = self
present(vc)
}
private func enterCredentials(forProfile profile: ConnectionProfile) {
let vc = StoryboardScene.Service.accountViewController.instantiate()
vc.profile = profile
vc.delegate = self
present(vc)
}
// MARK: Helpers
private func present(_ presentedViewController: NSViewController) {
viewController?.presentAsSheet(presentedViewController)
}
private func dismiss(_ presentedViewController: NSViewController) {
viewController?.dismiss(presentedViewController)
}
// XXX: copy/paste from iOS
private static func localizedMessage(forError error: Error) -> String {
if let appError = error as? ConfigurationError {
switch appError {
case .malformed(let option):
log.error("Could not parse configuration URL: malformed option, \(option)")
return L10n.ParsedFile.Alerts.Malformed.message(option)
case .missingConfiguration(let option):
log.error("Could not parse configuration URL: missing configuration, \(option)")
return L10n.ParsedFile.Alerts.Missing.message(option)
case .unsupportedConfiguration(let option):
log.error("Could not parse configuration URL: unsupported configuration, \(option)")
return L10n.ParsedFile.Alerts.Unsupported.message(option)
default:
break
}
}
log.error("Could not parse configuration URL: \(error)")
return L10n.ParsedFile.Alerts.Parsing.message(error.localizedDescription)
}
// XXX: copy/paste from iOS
private static func localizedDetailsMessage(forWarning warning: ConfigurationError) -> String {
switch warning {
case .malformed(let option):
return option
case .missingConfiguration(let option):
return option
case .unsupportedConfiguration(let option):
return option
default:
return "" // XXX: should never get here
}
}
}
extension HostImporter: TextInputViewControllerDelegate {
func textInputController(_ textInputController: TextInputViewController, shouldEnterText text: String) -> Bool {
// rename profile
guard let _ = textInputController.object as? ConnectionProfile else {
return true
}
return true//text.rangeOfCharacter(from: CharacterSet.filename.inverted) == nil
}
func textInputController(_ textInputController: TextInputViewController, didEnterText text: String) {
// rename profile
if let profile = textInputController.object as? ConnectionProfile {
createdTitle = text
// overwrite host with existing name?
replacedProfile = nil
if let existingHostId = service.existingHostId(withTitle: text) {
dismiss(textInputController)
let alert = Macros.warning(text, L10n.Wizards.Host.Alerts.Existing.message)
if alert.presentModally(withOK: L10n.Global.ok, cancel: L10n.Global.cancel) {
guard let existingProfile = service.profile(withContext: profile.context, id: existingHostId) else {
fatalError("ConnectionService.existingHostId() returned a non-existing host profile?")
}
replacedProfile = existingProfile
enterCredentials(forProfile: profile)
}
return
}
enterCredentials(forProfile: profile)
}
// enter passphrase
else {
importHost(withPassphrase: text)
}
dismiss(textInputController)
}
}
// enrich delegate
extension HostImporter : AccountViewControllerDelegate {
func accountController(_ accountController: AccountViewController, shouldUpdateCredentials credentials: Credentials, forProfile profile: ConnectionProfile) -> Bool {
return accountDelegate?.accountController(accountController, shouldUpdateCredentials: credentials, forProfile: profile) ?? true
}
func accountController(_ accountController: AccountViewController, didUpdateCredentials credentials: Credentials, forProfile profile: ConnectionProfile) {
if let replacedProfile = replacedProfile {
service.removeProfile(ProfileKey(replacedProfile))
}
service.addOrReplaceProfile(profile, credentials: credentials, title: createdTitle)
_ = try? service.save(configurationURL: configurationURL, for: profile)
accountDelegate?.accountController(accountController, didUpdateCredentials: credentials, forProfile: profile)
}
func accountControllerDidCancel(_ accountController: AccountViewController) {
accountDelegate?.accountControllerDidCancel(accountController)
}
}