passepartout-apple/Passepartout-iOS/Scenes/ServiceViewController.swift

1314 lines
46 KiB
Swift
Raw Normal View History

2018-10-11 07:13:19 +00:00
//
// ServiceViewController.swift
// Passepartout-iOS
//
// Created by Davide De Rosa on 6/6/18.
2019-03-09 10:44:44 +00:00
// Copyright (c) 2019 Davide De Rosa. All rights reserved.
2018-10-11 07:13:19 +00:00
//
2018-11-03 21:33:30 +00:00
// https://github.com/passepartoutvpn
2018-10-11 07:13:19 +00:00
//
// 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 UIKit
import NetworkExtension
2019-04-10 13:27:18 +00:00
import MBProgressHUD
2018-10-11 07:13:19 +00:00
import TunnelKit
import PassepartoutCore
2018-10-11 07:13:19 +00:00
class ServiceViewController: UIViewController, TableModelHost {
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var viewWelcome: UIView!
@IBOutlet private weak var labelWelcome: UILabel!
2018-11-03 21:23:26 +00:00
@IBOutlet private weak var itemEdit: UIBarButtonItem!
private var profile: ConnectionProfile?
2018-10-11 07:13:19 +00:00
private let service = TransientStore.shared.service
private lazy var vpn = GracefulVPN(service: service)
private weak var pendingRenameAction: UIAlertAction?
2018-10-11 07:13:19 +00:00
private var lastInfrastructureUpdate: Date?
private var shouldDeleteLogOnDisconnection = false
private var currentDataCount: (Int, Int)?
2018-10-11 07:13:19 +00:00
// MARK: Table
var model: TableModel<SectionType, RowType> = TableModel()
private let trustedNetworks = TrustedNetworksModel()
// MARK: UIViewController
deinit {
NotificationCenter.default.removeObserver(self)
}
func setProfile(_ profile: ConnectionProfile?, reloadingViews: Bool = true) {
self.profile = profile
vpn.profile = profile
title = profile?.id
navigationItem.rightBarButtonItem = (profile?.context == .host) ? itemEdit : nil
if reloadingViews {
reloadModel()
updateViewsIfNeeded()
}
}
2018-10-11 07:13:19 +00:00
override func awakeFromNib() {
super.awakeFromNib()
applyDetailTitle(Theme.current)
}
override func viewDidLoad() {
super.viewDidLoad()
// fall back to active profile
if profile == nil {
setProfile(service.activeProfile)
2018-10-11 07:13:19 +00:00
}
if let providerProfile = profile as? ProviderConnectionProfile {
lastInfrastructureUpdate = InfrastructureFactory.shared.modificationDate(for: providerProfile.name)
}
title = profile?.id
2018-10-11 07:13:19 +00:00
navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
navigationItem.leftItemsSupplementBackButton = true
2019-06-23 08:33:43 +00:00
labelWelcome.text = L10n.Core.Service.Welcome.message
2018-10-11 07:13:19 +00:00
labelWelcome.apply(Theme.current)
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
nc.addObserver(self, selector: #selector(vpnDidUpdate), name: .VPNDidChangeStatus, object: nil)
nc.addObserver(self, selector: #selector(vpnDidUpdate), name: .VPNDidReinstall, object: nil)
nc.addObserver(self, selector: #selector(intentDidUpdateService), name: .IntentDidUpdateService, object: nil)
nc.addObserver(self, selector: #selector(serviceDidUpdateDataCount(_:)), name: .ConnectionServiceDidUpdateDataCount, object: nil)
2018-10-11 07:13:19 +00:00
// run this no matter what
// XXX: convenient here vs AppDelegate for updating table
vpn.prepare() {
2018-10-21 08:05:39 +00:00
self.reloadModel()
self.updateViewsIfNeeded()
2018-10-11 07:13:19 +00:00
}
updateViewsIfNeeded()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
hideProfileIfDeleted()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
clearSelection()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let sid = segue.identifier, let segueType = StoryboardSegue.Main(rawValue: sid) else {
return
}
let destination = segue.destination
switch segueType {
case .accountSegueIdentifier:
let vc = destination as? AccountViewController
vc?.currentCredentials = service.credentials(for: uncheckedProfile)
vc?.usernamePlaceholder = (profile as? ProviderConnectionProfile)?.infrastructure.defaults.username
vc?.infrastructureName = (profile as? ProviderConnectionProfile)?.infrastructure.name
vc?.delegate = self
case .providerPoolSegueIdentifier:
let vc = destination as? ProviderPoolViewController
2019-04-25 18:40:50 +00:00
vc?.setInfrastructure(uncheckedProviderProfile.infrastructure, currentPoolId: uncheckedProviderProfile.poolId)
2018-10-11 07:13:19 +00:00
vc?.delegate = self
case .endpointSegueIdentifier:
let vc = destination as? EndpointViewController
vc?.dataSource = profile
vc?.delegate = self
vc?.modificationDelegate = self
case .providerPresetSegueIdentifier:
let infra = uncheckedProviderProfile.infrastructure
let presets: [InfrastructurePreset] = uncheckedProviderProfile.pool?.supportedPresetIds(in: uncheckedProviderProfile.infrastructure).map {
return infra.preset(for: $0)!
} ?? []
2018-10-11 07:13:19 +00:00
let vc = destination as? ProviderPresetViewController
vc?.presets = presets
2018-10-11 07:13:19 +00:00
vc?.currentPresetId = uncheckedProviderProfile.presetId
vc?.delegate = self
case .hostParametersSegueIdentifier:
let vc = destination as? ConfigurationViewController
vc?.title = L10n.App.Service.Cells.Host.Parameters.caption
vc?.initialConfiguration = uncheckedHostProfile.parameters.sessionConfiguration
vc?.originalConfigurationURL = service.configurationURL(for: uncheckedHostProfile)
2018-10-11 07:13:19 +00:00
vc?.delegate = self
case .networkSettingsSegueIdentifier:
let vc = destination as? NetworkSettingsViewController
vc?.title = L10n.Core.NetworkSettings.title
vc?.profile = profile
2018-10-11 07:13:19 +00:00
case .debugLogSegueIdentifier:
break
}
}
// MARK: Actions
func hideProfileIfDeleted() {
guard let profile = profile else {
return
}
if !service.containsProfile(profile) {
setProfile(nil)
2018-10-11 07:13:19 +00:00
}
}
// XXX: outlets can be nil here!
private func updateViewsIfNeeded() {
tableView?.reloadData()
viewWelcome?.isHidden = (profile != nil)
}
private func activateProfile() {
service.activateProfile(uncheckedProfile)
// for vpn methods to work, must update .profile to currently active profile
vpn.profile = uncheckedProfile
vpn.disconnect { (error) in
self.reloadModel()
self.updateViewsIfNeeded()
}
2018-10-11 07:13:19 +00:00
}
@IBAction private func renameProfile() {
let alert = Macros.alert(L10n.Core.Service.Alerts.Rename.title, L10n.Core.Global.Host.TitleInput.message)
alert.addTextField { (field) in
field.text = self.profile?.id
field.applyProfileId(Theme.current)
field.delegate = self
}
pendingRenameAction = alert.addDefaultAction(L10n.Core.Global.ok) {
guard let newId = alert.textFields?.first?.text else {
return
}
self.doRenameCurrentProfile(to: newId)
}
alert.addCancelAction(L10n.Core.Global.cancel)
pendingRenameAction?.isEnabled = false
present(alert, animated: true, completion: nil)
}
private func doRenameCurrentProfile(to newId: String) {
let renamedProfile = service.renameProfile(uncheckedHostProfile, to: newId)
setProfile(renamedProfile, reloadingViews: false)
}
2018-10-11 07:13:19 +00:00
private func toggleVpnService(cell: ToggleTableViewCell) {
if cell.isOn {
if #available(iOS 12, *) {
IntentDispatcher.donateConnection(with: uncheckedProfile)
}
guard !service.needsCredentials(for: uncheckedProfile) else {
2018-10-11 07:13:19 +00:00
let alert = Macros.alert(
L10n.App.Service.Sections.Vpn.header,
L10n.Core.Service.Alerts.CredentialsNeeded.message
2018-10-11 07:13:19 +00:00
)
alert.addCancelAction(L10n.Core.Global.ok) {
2018-10-11 07:13:19 +00:00
cell.setOn(false, animated: true)
}
present(alert, animated: true, completion: nil)
return
}
vpn.reconnect { (error) in
guard error == nil else {
// XXX: delay to avoid weird toggle state
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
cell.setOn(false, animated: true)
if error as? ApplicationError == .externalResources {
self.requireDownload()
}
}
2018-10-11 07:13:19 +00:00
return
}
2018-10-21 08:05:39 +00:00
self.reloadModel()
self.updateViewsIfNeeded()
2018-10-11 07:13:19 +00:00
}
} else {
if #available(iOS 12, *) {
IntentDispatcher.donateDisableVPN()
}
2018-10-11 07:13:19 +00:00
vpn.disconnect { (error) in
2018-10-21 08:05:39 +00:00
self.reloadModel()
self.updateViewsIfNeeded()
2018-10-11 07:13:19 +00:00
}
}
}
private func confirmVpnReconnection() {
guard vpn.status == .disconnected else {
let alert = Macros.alert(
L10n.Core.Service.Cells.ConnectionStatus.caption,
L10n.Core.Service.Alerts.ReconnectVpn.message
2018-10-11 07:13:19 +00:00
)
alert.addDefaultAction(L10n.Core.Global.ok) {
2018-10-11 07:13:19 +00:00
self.vpn.reconnect(completionHandler: nil)
}
alert.addCancelAction(L10n.Core.Global.cancel)
2018-10-11 07:13:19 +00:00
present(alert, animated: true, completion: nil)
return
}
vpn.reconnect(completionHandler: nil)
}
private func refreshProviderInfrastructure() {
let name = uncheckedProviderProfile.name
2018-10-11 07:13:19 +00:00
let hud = HUD()
let isUpdating = InfrastructureFactory.shared.update(name, notBeforeInterval: AppConstants.Web.minimumUpdateInterval) { (response, error) in
2018-10-11 07:13:19 +00:00
hud.hide()
guard let response = response else {
return
}
self.lastInfrastructureUpdate = response.1
self.tableView.reloadData()
}
if !isUpdating {
hud.hide()
}
}
private func toggleDisconnectsOnSleep(_ isOn: Bool) {
service.preferences.disconnectsOnSleep = !isOn
if vpn.isEnabled {
vpn.reinstall(completionHandler: nil)
}
}
private func toggleResolvesHostname(_ isOn: Bool) {
service.preferences.resolvesHostname = isOn
if vpn.isEnabled {
guard vpn.status == .disconnected else {
confirmVpnReconnection()
return
}
vpn.reinstall(completionHandler: nil)
}
}
private func toggleTrustedConnectionPolicy(_ isOn: Bool, sender: ToggleTableViewCell) {
2018-10-11 07:13:19 +00:00
let completionHandler: () -> Void = {
self.service.preferences.trustPolicy = isOn ? .disconnect : .ignore
2018-10-11 07:13:19 +00:00
if self.vpn.isEnabled {
self.vpn.reinstall(completionHandler: nil)
}
}
guard isOn else {
2018-10-11 07:13:19 +00:00
completionHandler()
return
}
guard vpn.isEnabled else {
completionHandler()
return
}
2018-10-11 07:13:19 +00:00
let alert = Macros.alert(
L10n.Core.Service.Sections.Trusted.header,
L10n.Core.Service.Alerts.Trusted.WillDisconnectPolicy.message
2018-10-11 07:13:19 +00:00
)
alert.addDefaultAction(L10n.Core.Global.ok) {
2018-10-11 07:13:19 +00:00
completionHandler()
}
alert.addCancelAction(L10n.Core.Global.cancel) {
sender.setOn(false, animated: true)
2018-10-11 07:13:19 +00:00
}
present(alert, animated: true, completion: nil)
}
private func confirmPotentialTrustedDisconnection(at rowIndex: Int?, completionHandler: @escaping () -> Void) {
let alert = Macros.alert(
L10n.Core.Service.Sections.Trusted.header,
L10n.Core.Service.Alerts.Trusted.WillDisconnectTrusted.message
2018-10-11 07:13:19 +00:00
)
alert.addDefaultAction(L10n.Core.Global.ok) {
2018-10-11 07:13:19 +00:00
completionHandler()
}
alert.addCancelAction(L10n.Core.Global.cancel) {
2018-10-11 07:13:19 +00:00
guard let rowIndex = rowIndex else {
return
}
let indexPath = IndexPath(row: rowIndex, section: self.trustedSectionIndex)
let cell = self.tableView.cellForRow(at: indexPath) as? ToggleTableViewCell
cell?.setOn(false, animated: true)
}
present(alert, animated: true, completion: nil)
}
private func testInternetConnectivity() {
let hud = HUD()
Utils.checkConnectivityURL(AppConstants.Web.connectivityURL, timeout: AppConstants.Web.connectivityTimeout) {
2018-10-11 07:13:19 +00:00
hud.hide()
let V = L10n.Core.Service.Alerts.TestConnectivity.Messages.self
2018-10-11 07:13:19 +00:00
let alert = Macros.alert(
L10n.Core.Service.Alerts.TestConnectivity.title,
2018-10-11 07:13:19 +00:00
$0 ? V.success : V.failure
)
alert.addCancelAction(L10n.Core.Global.ok)
2018-10-11 07:13:19 +00:00
self.present(alert, animated: true, completion: nil)
}
}
// private func displayDataCount() {
// guard vpn.isEnabled else {
// let alert = Macros.alert(
// L10n.Core.Service.Cells.DataCount.caption,
// L10n.Core.Service.Alerts.DataCount.Messages.notAvailable
// )
// alert.addCancelAction(L10n.Core.Global.ok)
// present(alert, animated: true, completion: nil)
// return
// }
//
// vpn.requestBytesCount {
// let message: String
// if let count = $0 {
// message = L10n.Core.Service.Alerts.DataCount.Messages.current(Int(count.0), Int(count.1))
// } else {
// message = L10n.Core.Service.Alerts.DataCount.Messages.notAvailable
// }
// let alert = Macros.alert(
// L10n.Core.Service.Cells.DataCount.caption,
// message
// )
// alert.addCancelAction(L10n.Core.Global.ok)
// self.present(alert, animated: true, completion: nil)
// }
// }
private func togglePrivateDataMasking(cell: ToggleTableViewCell) {
let handler = {
TransientStore.masksPrivateData = cell.isOn
self.service.baseConfiguration = TransientStore.baseVPNConfiguration.build()
}
guard vpn.status == .disconnected else {
let alert = Macros.alert(
L10n.Core.Service.Cells.MasksPrivateData.caption,
L10n.Core.Service.Alerts.MasksPrivateData.Messages.mustReconnect
)
alert.addDestructiveAction(L10n.Core.Service.Alerts.Buttons.reconnect) {
handler()
self.shouldDeleteLogOnDisconnection = true
2019-03-22 18:25:01 +00:00
self.vpn.reconnect(completionHandler: nil)
}
alert.addCancelAction(L10n.Core.Global.cancel) {
cell.setOn(!cell.isOn, animated: true)
}
present(alert, animated: true, completion: nil)
return
}
handler()
service.eraseVpnLog()
shouldDeleteLogOnDisconnection = false
}
2018-10-11 07:13:19 +00:00
private func reportConnectivityIssue() {
let attach = IssueReporter.Attachments(debugLog: true, profile: uncheckedProfile)
IssueReporter.shared.present(in: self, withAttachments: attach)
2018-10-11 07:13:19 +00:00
}
private func requireDownload() {
guard let providerProfile = profile as? ProviderConnectionProfile else {
return
}
guard let downloadURL = AppConstants.URLs.externalResources[providerProfile.name] else {
return
}
let alert = Macros.alert(
L10n.Core.Service.Alerts.Download.title,
L10n.Core.Service.Alerts.Download.message(providerProfile.name.rawValue)
)
alert.addCancelAction(L10n.Core.Global.cancel)
alert.addDefaultAction(L10n.Core.Global.ok) {
2019-04-10 13:27:18 +00:00
self.confirmDownload(URL(string: downloadURL)!)
}
present(alert, animated: true, completion: nil)
}
2019-04-10 13:27:18 +00:00
private func confirmDownload(_ url: URL) {
_ = Downloader.shared.download(url: url, in: view) { (url, error) in
self.handleDownloadedProviderResources(url: url, error: error)
}
}
private func handleDownloadedProviderResources(url: URL?, error: Error?) {
guard let url = url else {
let alert = Macros.alert(
L10n.Core.Service.Alerts.Download.title,
L10n.Core.Service.Alerts.Download.failed(error?.localizedDescription ?? "")
)
alert.addCancelAction(L10n.Core.Global.ok)
present(alert, animated: true, completion: nil)
return
}
let hud = HUD(label: L10n.Core.Service.Alerts.Download.Hud.extracting)
hud.show()
uncheckedProviderProfile.name.importExternalResources(from: url) {
hud.hide()
2019-04-10 13:27:18 +00:00
}
}
2018-10-11 07:13:19 +00:00
// MARK: Notifications
@objc private func vpnDidUpdate() {
reloadVpnStatus()
guard let status = vpn.status else {
return
}
switch status {
case .connected:
Reviewer.shared.reportEvent()
case .disconnected:
if shouldDeleteLogOnDisconnection {
service.eraseVpnLog()
shouldDeleteLogOnDisconnection = false
}
default:
break
}
2018-10-11 07:13:19 +00:00
}
@objc private func intentDidUpdateService() {
setProfile(service.activeProfile)
}
2018-10-11 07:13:19 +00:00
@objc private func applicationDidBecomeActive() {
reloadModel()
updateViewsIfNeeded()
2018-10-11 07:13:19 +00:00
}
@objc private func serviceDidUpdateDataCount(_ notification: Notification) {
guard let dataCount = notification.userInfo?[ConnectionService.NotificationKeys.dataCount] as? (Int, Int) else {
return
}
refreshDataCount(dataCount)
}
2018-10-11 07:13:19 +00:00
}
// MARK: -
extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, ToggleTableViewCellDelegate {
enum SectionType {
case vpn
case authentication
case hostProfile
case configuration
case providerInfrastructure
case vpnResolvesHostname
case vpnSurvivesSleep
case trusted
case trustedPolicy
case diagnostics
2018-10-18 14:53:37 +00:00
case feedback
2018-10-11 07:13:19 +00:00
}
enum RowType: Int {
2018-10-21 07:45:59 +00:00
case useProfile
2018-10-11 07:13:19 +00:00
case vpnService
case connectionStatus
2018-10-21 07:45:59 +00:00
case reconnect
2018-10-11 07:13:19 +00:00
case account
case endpoint
case providerPool
case providerPreset
case providerRefresh
case hostParameters
case networkSettings
2018-10-11 07:13:19 +00:00
case vpnResolvesHostname
case vpnSurvivesSleep
case trustedMobile
case trustedWiFi
case trustedAddCurrentWiFi
case trustedPolicy
case testConnectivity
case dataCount
case debugLog
case masksPrivateData
case reportIssue
2018-10-11 07:13:19 +00:00
}
private var trustedSectionIndex: Int {
return model.index(ofSection: .trusted)
}
private var statusIndexPath: IndexPath? {
return model.indexPath(row: .connectionStatus, section: .vpn)
2018-10-11 07:13:19 +00:00
}
private var dataCountIndexPath: IndexPath? {
return model.indexPath(row: .dataCount, section: .diagnostics)
}
2018-10-11 07:13:19 +00:00
private var endpointIndexPath: IndexPath {
guard let ip = model.indexPath(row: .endpoint, section: .configuration) else {
fatalError("Could not locate endpointIndexPath")
}
return ip
}
private var providerPresetIndexPath: IndexPath {
guard let ip = model.indexPath(row: .providerPreset, section: .configuration) else {
fatalError("Could not locate presetIndexPath")
}
return ip
}
2019-03-18 10:52:19 +00:00
private func mappedTrustedNetworksRow(_ from: TrustedNetworksModel.RowType) -> RowType {
switch from {
case .trustsMobile:
return .trustedMobile
case .trustedWiFi:
return .trustedWiFi
case .addCurrentWiFi:
return .trustedAddCurrentWiFi
}
}
2018-10-11 07:13:19 +00:00
func numberOfSections(in tableView: UITableView) -> Int {
return model.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return model.header(for: section)
}
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
let rows = model.rows(for: section)
if rows.contains(.providerRefresh), let date = lastInfrastructureUpdate {
return L10n.Core.Service.Sections.ProviderInfrastructure.footer(date.timestamp)
2018-10-11 07:13:19 +00:00
}
return model.footer(for: section)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return model.headerHeight(for: section)
}
2018-10-11 07:13:19 +00:00
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count(for: section)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = model.row(at: indexPath)
switch row {
2018-10-21 07:45:59 +00:00
case .useProfile:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.applyAction(Theme.current)
cell.leftText = L10n.App.Service.Cells.UseProfile.caption
2018-10-21 07:45:59 +00:00
return cell
2018-10-11 07:13:19 +00:00
case .vpnService:
guard service.isActiveProfile(uncheckedProfile) else {
fatalError("Do not show vpnService in non-active profile")
}
let cell = Cells.toggle.dequeue(from: tableView, for: indexPath, tag: row.rawValue, delegate: self)
cell.caption = L10n.App.Service.Cells.VpnService.caption
2018-10-11 07:13:19 +00:00
cell.isOn = vpn.isEnabled
return cell
case .connectionStatus:
guard service.isActiveProfile(uncheckedProfile) else {
fatalError("Do not show connectionStatus in non-active profile")
}
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.applyVPN(Theme.current, with: vpn.isEnabled ? vpn.status : nil, error: service.vpnLastError)
cell.leftText = L10n.Core.Service.Cells.ConnectionStatus.caption
2018-10-11 07:13:19 +00:00
cell.accessoryType = .none
2018-10-21 08:05:39 +00:00
cell.isTappable = false
2018-10-11 07:13:19 +00:00
return cell
2018-10-21 07:45:59 +00:00
case .reconnect:
2018-10-11 07:13:19 +00:00
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.applyAction(Theme.current)
cell.leftText = L10n.App.Service.Cells.Reconnect.caption
2018-10-21 07:45:59 +00:00
cell.accessoryType = .none
cell.isTappable = !service.needsCredentials(for: uncheckedProfile) && vpn.isEnabled
2018-10-11 07:13:19 +00:00
return cell
// shared cells
case .account:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.Account.title
2018-10-11 07:13:19 +00:00
cell.rightText = profile?.username
return cell
case .endpoint:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.Endpoint.title
2018-10-11 07:13:19 +00:00
let V = L10n.Core.Global.Values.self
2018-10-11 07:13:19 +00:00
if let provider = profile as? ProviderConnectionProfile {
cell.rightText = provider.usesProviderEndpoint ? V.manual : V.automatic
} else {
cell.rightText = profile?.mainAddress
}
return cell
case .networkSettings:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.NetworkSettings.title
return cell
2018-10-11 07:13:19 +00:00
// provider cells
case .providerPool:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.Service.Cells.Provider.Pool.caption
cell.rightText = uncheckedProviderProfile.pool?.localizedId
2018-10-11 07:13:19 +00:00
return cell
case .providerPreset:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.Service.Cells.Provider.Preset.caption
2018-10-11 07:13:19 +00:00
cell.rightText = uncheckedProviderProfile.preset?.name // XXX: localize?
return cell
case .providerRefresh:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.applyAction(Theme.current)
cell.leftText = L10n.App.Service.Cells.Provider.Refresh.caption
2018-10-11 07:13:19 +00:00
return cell
// host cells
case .hostParameters:
let parameters = uncheckedHostProfile.parameters
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.App.Service.Cells.Host.Parameters.caption
if !parameters.sessionConfiguration.fallbackCipher.embedsDigest {
2019-04-23 13:19:53 +00:00
cell.rightText = "\(parameters.sessionConfiguration.fallbackCipher.genericName) / \(parameters.sessionConfiguration.fallbackDigest.genericName)"
2018-10-11 07:13:19 +00:00
} else {
2019-04-23 13:19:53 +00:00
cell.rightText = parameters.sessionConfiguration.fallbackCipher.genericName
2018-10-11 07:13:19 +00:00
}
return cell
// VPN preferences
case .vpnResolvesHostname:
let cell = Cells.toggle.dequeue(from: tableView, for: indexPath, tag: row.rawValue, delegate: self)
cell.caption = L10n.Core.Service.Cells.VpnResolvesHostname.caption
2018-10-11 07:13:19 +00:00
cell.isOn = service.preferences.resolvesHostname
return cell
case .vpnSurvivesSleep:
let cell = Cells.toggle.dequeue(from: tableView, for: indexPath, tag: row.rawValue, delegate: self)
cell.caption = L10n.Core.Service.Cells.VpnSurvivesSleep.caption
2018-10-11 07:13:19 +00:00
cell.isOn = !service.preferences.disconnectsOnSleep
return cell
case .trustedMobile:
let cell = Cells.toggle.dequeue(from: tableView, for: indexPath, tag: row.rawValue, delegate: self)
cell.caption = L10n.Core.Service.Cells.TrustedMobile.caption
2018-10-11 07:13:19 +00:00
cell.isOn = service.preferences.trustsMobileNetwork
return cell
case .trustedWiFi:
let wifi = trustedNetworks.wifi(at: indexPath.row)
let cell = Cells.toggle.dequeue(from: tableView, for: indexPath, tag: row.rawValue, delegate: self)
2019-04-23 13:19:53 +00:00
cell.caption = wifi.0
2018-10-11 07:13:19 +00:00
cell.isOn = wifi.1
return cell
case .trustedAddCurrentWiFi:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.applyAction(Theme.current)
cell.leftText = L10n.App.Service.Cells.TrustedAddWifi.caption
2018-10-11 07:13:19 +00:00
return cell
case .trustedPolicy:
let cell = Cells.toggle.dequeue(from: tableView, for: indexPath, tag: row.rawValue, delegate: self)
cell.caption = L10n.Core.Service.Cells.TrustedPolicy.caption
cell.isOn = (service.preferences.trustPolicy == .disconnect)
2018-10-11 07:13:19 +00:00
return cell
// diagnostics
case .testConnectivity:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.Service.Cells.TestConnectivity.caption
2018-10-11 07:13:19 +00:00
return cell
case .dataCount:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.Service.Cells.DataCount.caption
if let count = currentDataCount, vpn.status == .connected {
2019-04-23 13:19:53 +00:00
let down = count.0.dataUnitDescription
let up = count.1.dataUnitDescription
cell.rightText = "\(down) / ↑\(up)"
} else {
cell.rightText = L10n.Core.Service.Cells.DataCount.none
}
cell.accessoryType = .none
cell.isTappable = false
2018-10-11 07:13:19 +00:00
return cell
case .debugLog:
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.Service.Cells.DebugLog.caption
2018-10-11 07:13:19 +00:00
return cell
case .masksPrivateData:
let cell = Cells.toggle.dequeue(from: tableView, for: indexPath, tag: row.rawValue, delegate: self)
cell.caption = L10n.Core.Service.Cells.MasksPrivateData.caption
cell.isOn = TransientStore.masksPrivateData
return cell
2018-10-18 14:53:37 +00:00
// feedback
2018-10-11 07:13:19 +00:00
case .reportIssue:
2018-10-11 07:13:19 +00:00
let cell = Cells.setting.dequeue(from: tableView, for: indexPath)
cell.leftText = L10n.Core.Service.Cells.ReportIssue.caption
2018-10-11 07:13:19 +00:00
return cell
}
}
// func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// cell.isSelected = (indexPath == lastSelectedIndexPath)
// }
// MARK: Actions
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard let cell = tableView.cellForRow(at: indexPath) else {
return nil
}
if let settingCell = cell as? SettingTableViewCell {
guard settingCell.isTappable else {
return nil
}
}
guard handle(row: model.row(at: indexPath), cell: cell) else {
return nil
}
return indexPath
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return model.row(at: indexPath) == .trustedWiFi
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
precondition(indexPath.section == model.index(ofSection: .trusted))
trustedNetworks.removeWifi(at: indexPath.row)
}
func toggleCell(_ cell: ToggleTableViewCell, didToggleToValue value: Bool) {
guard let item = RowType(rawValue: cell.tag) else {
return
}
handle(row: item, cell: cell)
}
// true if enters subscreen
private func handle(row: RowType, cell: UITableViewCell) -> Bool {
switch row {
case .useProfile:
activateProfile()
2018-10-11 07:13:19 +00:00
2018-10-21 07:45:59 +00:00
case .reconnect:
confirmVpnReconnection()
2018-10-11 07:13:19 +00:00
case .account:
perform(segue: StoryboardSegue.Main.accountSegueIdentifier, sender: cell)
return true
case .endpoint:
perform(segue: StoryboardSegue.Main.endpointSegueIdentifier, sender: cell)
return true
case .providerPool:
perform(segue: StoryboardSegue.Main.providerPoolSegueIdentifier, sender: cell)
return true
case .providerPreset:
perform(segue: StoryboardSegue.Main.providerPresetSegueIdentifier, sender: cell)
return true
case .providerRefresh:
refreshProviderInfrastructure()
return false
case .hostParameters:
perform(segue: StoryboardSegue.Main.hostParametersSegueIdentifier, sender: cell)
return true
case .networkSettings:
perform(segue: StoryboardSegue.Main.networkSettingsSegueIdentifier, sender: cell)
return true
2018-10-11 07:13:19 +00:00
case .trustedAddCurrentWiFi:
if #available(iOS 12, *) {
IntentDispatcher.donateTrustCurrentNetwork()
IntentDispatcher.donateUntrustCurrentNetwork()
}
2018-10-11 07:13:19 +00:00
guard trustedNetworks.addCurrentWifi() else {
let alert = Macros.alert(
L10n.Core.Service.Sections.Trusted.header,
L10n.Core.Service.Alerts.Trusted.NoNetwork.message
2018-10-11 07:13:19 +00:00
)
alert.addCancelAction(L10n.Core.Global.ok)
2018-10-11 07:13:19 +00:00
present(alert, animated: true, completion: nil)
return false
}
case .testConnectivity:
testInternetConnectivity()
// case .dataCount:
// displayDataCount()
2018-10-11 07:13:19 +00:00
case .debugLog:
perform(segue: StoryboardSegue.Main.debugLogSegueIdentifier, sender: cell)
return true
case .reportIssue:
reportConnectivityIssue()
2018-10-11 07:13:19 +00:00
default:
break
}
return false
}
private func handle(row: RowType, cell: ToggleTableViewCell) {
switch row {
case .vpnService:
toggleVpnService(cell: cell)
case .vpnResolvesHostname:
toggleResolvesHostname(cell.isOn)
case .vpnSurvivesSleep:
toggleDisconnectsOnSleep(cell.isOn)
case .trustedMobile:
if #available(iOS 12, *) {
IntentDispatcher.donateTrustCellularNetwork()
IntentDispatcher.donateUntrustCellularNetwork()
}
2018-10-11 07:13:19 +00:00
trustedNetworks.setMobile(cell.isOn)
case .trustedWiFi:
guard let indexPath = tableView.indexPath(for: cell) else {
return
}
if cell.isOn {
trustedNetworks.enableWifi(at: indexPath.row)
} else {
trustedNetworks.disableWifi(at: indexPath.row)
}
case .trustedPolicy:
toggleTrustedConnectionPolicy(cell.isOn, sender: cell)
2018-10-11 07:13:19 +00:00
case .masksPrivateData:
togglePrivateDataMasking(cell: cell)
2018-10-11 07:13:19 +00:00
default:
break
}
}
// MARK: Updates
func reloadModel() {
model.clear()
guard let profile = profile else {
return
}
// assert(profile != nil, "Profile not set")
let isActiveProfile = service.isActiveProfile(profile)
let isProvider = (profile as? ProviderConnectionProfile) != nil
// sections
model.add(.vpn)
if isProvider {
model.add(.authentication)
}
model.add(.configuration)
if isProvider {
model.add(.providerInfrastructure)
}
if isActiveProfile {
if isProvider {
model.add(.vpnResolvesHostname)
}
model.add(.vpnSurvivesSleep)
model.add(.trusted)
model.add(.trustedPolicy)
model.add(.diagnostics)
2018-10-18 14:53:37 +00:00
model.add(.feedback)
2018-10-11 07:13:19 +00:00
}
// headers
model.setHeader(L10n.App.Service.Sections.Vpn.header, for: .vpn)
2018-10-11 07:13:19 +00:00
if isProvider {
model.setHeader(L10n.App.Service.Sections.Configuration.header, for: .authentication)
2018-10-11 07:13:19 +00:00
} else {
model.setHeader(L10n.App.Service.Sections.Configuration.header, for: .configuration)
2018-10-11 07:13:19 +00:00
}
if isActiveProfile {
if isProvider {
model.setHeader("", for: .vpnResolvesHostname)
model.setHeader("", for: .vpnSurvivesSleep)
}
model.setHeader(L10n.Core.Service.Sections.Trusted.header, for: .trusted)
model.setHeader(L10n.Core.Service.Sections.Diagnostics.header, for: .diagnostics)
model.setHeader(L10n.Core.Organizer.Sections.Feedback.header, for: .feedback)
2018-10-11 07:13:19 +00:00
}
// footers
if isActiveProfile {
model.setFooter(L10n.Core.Service.Sections.Vpn.footer, for: .vpn)
2018-10-11 07:13:19 +00:00
if isProvider {
model.setFooter(L10n.Core.Service.Sections.VpnResolvesHostname.footer, for: .vpnResolvesHostname)
2018-10-11 07:13:19 +00:00
}
model.setFooter(L10n.Core.Service.Sections.VpnSurvivesSleep.footer, for: .vpnSurvivesSleep)
model.setFooter(L10n.Core.Service.Sections.Trusted.footer, for: .trustedPolicy)
model.setFooter(L10n.Core.Service.Sections.Diagnostics.footer, for: .diagnostics)
2018-10-11 07:13:19 +00:00
}
// rows
if isActiveProfile {
2018-10-21 08:05:39 +00:00
var rows: [RowType] = [.vpnService, .connectionStatus]
if vpn.isEnabled {
rows.append(.reconnect)
}
model.set(rows, in: .vpn)
2018-10-11 07:13:19 +00:00
} else {
model.set([.useProfile], in: .vpn)
}
if isProvider {
model.set([.account], in: .authentication)
model.set([.providerPool, .endpoint, .providerPreset, .networkSettings], in: .configuration)
2018-10-11 07:13:19 +00:00
model.set([.providerRefresh], in: .providerInfrastructure)
} else {
model.set([.account, .endpoint, .hostParameters, .networkSettings], in: .configuration)
2018-10-11 07:13:19 +00:00
}
if isActiveProfile {
if isProvider {
model.set([.vpnResolvesHostname], in: .vpnResolvesHostname)
}
model.set([.vpnSurvivesSleep], in: .vpnSurvivesSleep)
model.set([.trustedPolicy], in: .trustedPolicy)
model.set([.dataCount, .debugLog, .masksPrivateData], in: .diagnostics)
2019-06-13 08:33:36 +00:00
model.set([.reportIssue], in: .feedback)
2018-10-11 07:13:19 +00:00
}
trustedNetworks.delegate = self
trustedNetworks.load(from: service.preferences)
2019-03-18 10:52:19 +00:00
model.set(trustedNetworks.rows.map { mappedTrustedNetworksRow($0) }, in: .trusted)
2018-10-11 07:13:19 +00:00
}
private func reloadVpnStatus() {
guard let profile = profile else {
return
}
guard service.isActiveProfile(profile) else {
return
}
var ips: [IndexPath] = []
guard let statusIndexPath = statusIndexPath else {
return
}
ips.append(statusIndexPath)
if let dataCountIndexPath = dataCountIndexPath {
currentDataCount = service.vpnDataCount
ips.append(dataCountIndexPath)
}
tableView.reloadRows(at: ips, with: .none)
}
private func refreshDataCount(_ dataCount: (Int, Int)?) {
currentDataCount = dataCount
guard let dataCountIndexPath = dataCountIndexPath else {
return
}
tableView.reloadRows(at: [dataCountIndexPath], with: .none)
2018-10-11 07:13:19 +00:00
}
func reloadSelectedRow(andRowsAt indexPaths: [IndexPath]? = nil) {
2018-10-11 07:13:19 +00:00
guard let selectedIP = tableView.indexPathForSelectedRow else {
return
}
var outdatedIPs = [selectedIP]
if let otherIPs = indexPaths {
outdatedIPs.append(contentsOf: otherIPs)
2018-10-11 07:13:19 +00:00
}
tableView.reloadRows(at: outdatedIPs, with: .none)
tableView.selectRow(at: selectedIP, animated: false, scrollPosition: .none)
}
func clearSelection() {
guard let selected = tableView.indexPathForSelectedRow else {
return
}
tableView.deselectRow(at: selected, animated: true)
}
}
// MARK: -
extension ServiceViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard string.rangeOfCharacter(from: CharacterSet.filename.inverted) == nil else {
return false
}
if let text = textField.text {
let replacement = (text as NSString).replacingCharacters(in: range, with: string)
pendingRenameAction?.isEnabled = (replacement != uncheckedProfile.id)
}
return true
}
2018-11-02 15:23:34 +00:00
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return true
}
}
// MARK: -
2018-10-11 07:13:19 +00:00
extension ServiceViewController: TrustedNetworksModelDelegate {
func trustedNetworksCouldDisconnect(_: TrustedNetworksModel) -> Bool {
return (service.preferences.trustPolicy == .disconnect) && (vpn.status != .disconnected)
}
func trustedNetworksShouldConfirmDisconnection(_: TrustedNetworksModel, triggeredAt rowIndex: Int, completionHandler: @escaping () -> Void) {
confirmPotentialTrustedDisconnection(at: rowIndex, completionHandler: completionHandler)
}
func trustedNetworks(_: TrustedNetworksModel, shouldInsertWifiAt rowIndex: Int) {
2019-03-18 10:52:19 +00:00
model.set(trustedNetworks.rows.map { mappedTrustedNetworksRow($0) }, in: .trusted)
2018-10-11 07:13:19 +00:00
tableView.insertRows(at: [IndexPath(row: rowIndex, section: trustedSectionIndex)], with: .bottom)
}
func trustedNetworks(_: TrustedNetworksModel, shouldReloadWifiAt rowIndex: Int, isTrusted: Bool) {
let genericCell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: trustedSectionIndex))
guard let cell = genericCell as? ToggleTableViewCell else {
fatalError("Not a trusted Wi-Fi cell (\(type(of: genericCell)) != ToggleTableViewCell)")
}
guard isTrusted != cell.isOn else {
return
}
cell.setOn(isTrusted, animated: true)
}
func trustedNetworks(_: TrustedNetworksModel, shouldDeleteWifiAt rowIndex: Int) {
2019-03-18 10:52:19 +00:00
model.set(trustedNetworks.rows.map { mappedTrustedNetworksRow($0) }, in: .trusted)
2018-10-11 07:13:19 +00:00
tableView.deleteRows(at: [IndexPath(row: rowIndex, section: trustedSectionIndex)], with: .top)
}
func trustedNetworksShouldReinstall(_: TrustedNetworksModel) {
service.preferences.trustsMobileNetwork = trustedNetworks.trustsMobileNetwork
service.preferences.trustedWifis = trustedNetworks.trustedWifis
if vpn.isEnabled {
vpn.reinstall(completionHandler: nil)
}
}
}
// MARK: -
extension ServiceViewController: ConfigurationModificationDelegate {
func configuration(didUpdate newConfiguration: OpenVPN.Configuration) {
2018-10-11 07:13:19 +00:00
if let hostProfile = profile as? HostConnectionProfile {
var builder = hostProfile.parameters.builder()
builder.sessionConfiguration = newConfiguration
hostProfile.parameters = builder.build()
2018-10-11 07:13:19 +00:00
}
reloadSelectedRow()
}
func configurationShouldReinstall() {
vpn.reinstallIfEnabled()
}
}
extension ServiceViewController: AccountViewControllerDelegate {
func accountController(_ vc: AccountViewController, didEnterCredentials credentials: Credentials) {
}
func accountControllerDidComplete(_ accountVC: AccountViewController) {
navigationController?.popViewController(animated: true)
let credentials = accountVC.credentials
guard credentials != service.credentials(for: uncheckedProfile) else {
return
}
try? service.setCredentials(credentials, for: uncheckedProfile)
reloadSelectedRow()
vpn.reinstallIfEnabled()
}
}
extension ServiceViewController: EndpointViewControllerDelegate {
2018-11-10 09:29:51 +00:00
func endpointController(_: EndpointViewController, didUpdateWithNewAddress newAddress: String?, newProtocol: EndpointProtocol?) {
2018-10-11 07:13:19 +00:00
if let providerProfile = profile as? ProviderConnectionProfile {
providerProfile.manualAddress = newAddress
providerProfile.manualProtocol = newProtocol
}
reloadSelectedRow()
}
}
extension ServiceViewController: ProviderPoolViewControllerDelegate {
func providerPoolController(_ vc: ProviderPoolViewController, didSelectPool pool: Pool) {
navigationController?.popToViewController(self, animated: true)
2018-10-11 07:13:19 +00:00
guard pool.id != uncheckedProviderProfile.poolId else {
return
}
uncheckedProviderProfile.poolId = pool.id
var extraReloadedRows = [endpointIndexPath]
// fall back to a supported preset and reload preset row too
let supportedPresets = pool.supportedPresetIds(in: uncheckedProviderProfile.infrastructure)
if let presetId = uncheckedProviderProfile.preset?.id, !supportedPresets.contains(presetId),
let fallback = supportedPresets.first {
if fallback != uncheckedProviderProfile.presetId {
extraReloadedRows.append(providerPresetIndexPath)
}
uncheckedProviderProfile.presetId = fallback
}
reloadSelectedRow(andRowsAt: extraReloadedRows)
2018-10-11 07:13:19 +00:00
vpn.reinstallIfEnabled()
if #available(iOS 12, *) {
IntentDispatcher.donateConnection(with: uncheckedProviderProfile)
}
2018-10-11 07:13:19 +00:00
}
}
extension ServiceViewController: ProviderPresetViewControllerDelegate {
func providerPresetController(_: ProviderPresetViewController, didSelectPreset preset: InfrastructurePreset) {
navigationController?.popViewController(animated: true)
guard preset.id != uncheckedProviderProfile.presetId else {
return
}
uncheckedProviderProfile.presetId = preset.id
reloadSelectedRow(andRowsAt: [endpointIndexPath])
2018-10-11 07:13:19 +00:00
vpn.reinstallIfEnabled()
}
}
// MARK: -
private extension ServiceViewController {
private var uncheckedProfile: ConnectionProfile {
guard let profile = profile else {
fatalError("Expected non-nil profile here")
}
return profile
}
private var uncheckedProviderProfile: ProviderConnectionProfile {
guard let profile = profile as? ProviderConnectionProfile else {
fatalError("Expected ProviderConnectionProfile (found: \(type(of: self.profile)))")
}
return profile
}
private var uncheckedHostProfile: HostConnectionProfile {
guard let profile = profile as? HostConnectionProfile else {
fatalError("Expected HostConnectionProfile (found: \(type(of: self.profile)))")
}
return profile
}
}