Extend authentication methods (#259)

* Add profile authentication method

- Persistent (default, fallback)

- Interactive (may expire through reconnections)

- TOTP (seed-based) - currently disabled

* Disable on-demand if login is interactive

* Present interactive prompt on VPN toggle
This commit is contained in:
Davide De Rosa 2023-03-17 16:49:35 +01:00 committed by GitHub
parent 44ccd21536
commit 2e10aab039
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 346 additions and 92 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Prompt for password interactively. [#3](https://github.com/passepartoutvpn/passepartout-apple/issues/3)
- Ukranian translations (Dmitry Chirkin). [#243](https://github.com/passepartoutvpn/passepartout-apple/pull/243) - Ukranian translations (Dmitry Chirkin). [#243](https://github.com/passepartoutvpn/passepartout-apple/pull/243)
- OpenVPN: Full implementation of Tunnelblick XOR patch (tmthecoder). [#245](https://github.com/passepartoutvpn/passepartout-apple/pull/245), [tunnelkit#255][https://github.com/passepartoutvpn/tunnelkit/pull/255] - OpenVPN: Full implementation of Tunnelblick XOR patch (tmthecoder). [#245](https://github.com/passepartoutvpn/passepartout-apple/pull/245), [tunnelkit#255][https://github.com/passepartoutvpn/tunnelkit/pull/255]

View File

@ -146,12 +146,13 @@
0EB34BCC27C6F41D00B126DA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB34BCB27C6F41D00B126DA /* Theme.swift */; }; 0EB34BCC27C6F41D00B126DA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB34BCB27C6F41D00B126DA /* Theme.swift */; };
0EB4042C27CA0E8C00378B1A /* Unlocalized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042B27CA0E8B00378B1A /* Unlocalized.swift */; }; 0EB4042C27CA0E8C00378B1A /* Unlocalized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042B27CA0E8B00378B1A /* Unlocalized.swift */; };
0EB4042E27CA136300378B1A /* AddingTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042D27CA136200378B1A /* AddingTextField.swift */; }; 0EB4042E27CA136300378B1A /* AddingTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB4042D27CA136200378B1A /* AddingTextField.swift */; };
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */; };
0EBC074C27EB673C00208AD9 /* ProfileView+Rename.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */; }; 0EBC074C27EB673C00208AD9 /* ProfileView+Rename.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */; };
0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075427EBC83800208AD9 /* MailComposerView.swift */; }; 0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075427EBC83800208AD9 /* MailComposerView.swift */; };
0EBC075B27EC4FFF00208AD9 /* ReportIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */; }; 0EBC075B27EC4FFF00208AD9 /* ReportIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */; };
0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075C27EC529000208AD9 /* DebugLog+Constants.swift */; }; 0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075C27EC529000208AD9 /* DebugLog+Constants.swift */; };
0EBC076027EC587900208AD9 /* SwiftGen+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */; }; 0EBC076027EC587900208AD9 /* SwiftGen+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */; };
0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */; }; 0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */; };
0ECB78E9285F5DE300B0E460 /* PassepartoutMac.bundle in Embed Plugins */ = {isa = PBXBuildFile; fileRef = 0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0ECB78E9285F5DE300B0E460 /* PassepartoutMac.bundle in Embed Plugins */ = {isa = PBXBuildFile; fileRef = 0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
0ECB78EC2863A21600B0E460 /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0ECB78EB2863A21600B0E460 /* PassepartoutLibrary */; }; 0ECB78EC2863A21600B0E460 /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0ECB78EB2863A21600B0E460 /* PassepartoutLibrary */; };
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF71ED27B6A99300CDB528 /* AccountView.swift */; }; 0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF71ED27B6A99300CDB528 /* AccountView.swift */; };
@ -445,6 +446,7 @@
0EB34BCB27C6F41D00B126DA /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; }; 0EB34BCB27C6F41D00B126DA /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
0EB4042B27CA0E8B00378B1A /* Unlocalized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unlocalized.swift; sourceTree = "<group>"; }; 0EB4042B27CA0E8B00378B1A /* Unlocalized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unlocalized.swift; sourceTree = "<group>"; };
0EB4042D27CA136200378B1A /* AddingTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddingTextField.swift; sourceTree = "<group>"; }; 0EB4042D27CA136200378B1A /* AddingTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddingTextField.swift; sourceTree = "<group>"; };
0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveConnectionView.swift; sourceTree = "<group>"; };
0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Rename.swift"; sourceTree = "<group>"; }; 0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Rename.swift"; sourceTree = "<group>"; };
0EBC075427EBC83800208AD9 /* MailComposerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailComposerView.swift; sourceTree = "<group>"; }; 0EBC075427EBC83800208AD9 /* MailComposerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailComposerView.swift; sourceTree = "<group>"; };
0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportIssueView.swift; sourceTree = "<group>"; }; 0EBC075A27EC4FFF00208AD9 /* ReportIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportIssueView.swift; sourceTree = "<group>"; };
@ -459,7 +461,7 @@
0EBE2FD62360F89500F0D5AB /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 0EBE2FD62360F89500F0D5AB /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
0EBE2FD72360F89600F0D5AB /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 0EBE2FD72360F89600F0D5AB /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
0EBE2FD82360F89600F0D5AB /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 0EBE2FD82360F89600F0D5AB /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; };
0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRow.swift; sourceTree = "<group>"; }; 0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+ProfileRow.swift"; sourceTree = "<group>"; };
0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PassepartoutMac.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PassepartoutMac.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
0ECB78E1285F53ED00B0E460 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 0ECB78E1285F53ED00B0E460 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
0ECB78EA2861D1F300B0E460 /* PassepartoutLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PassepartoutLibrary; sourceTree = "<group>"; }; 0ECB78EA2861D1F300B0E460 /* PassepartoutLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PassepartoutLibrary; sourceTree = "<group>"; };
@ -639,17 +641,18 @@
0E71ACEA27C1060D00F85C4B /* EndpointView.swift */, 0E71ACEA27C1060D00F85C4B /* EndpointView.swift */,
0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */, 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */,
0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */, 0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */,
0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */,
0E0BD27227B2EA2C00583AC5 /* MainView.swift */, 0E0BD27227B2EA2C00583AC5 /* MainView.swift */,
0E71ACE827C1055200F85C4B /* NetworkSettingsView.swift */, 0E71ACE827C1055200F85C4B /* NetworkSettingsView.swift */,
0EB34BC927C6A70200B126DA /* OnDemandView.swift */, 0EB34BC927C6A70200B126DA /* OnDemandView.swift */,
0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */, 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */,
0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */, 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */,
0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */,
0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */, 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */,
0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */, 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */,
0EF0FAF527DD0211007EB181 /* PaywallView.swift */, 0EF0FAF527DD0211007EB181 /* PaywallView.swift */,
0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */, 0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */,
0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */, 0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */,
0EBE880E281B18DE0090D9E6 /* ProfileRow.swift */,
0E44689527B051C300A14CE4 /* ProfileView.swift */, 0E44689527B051C300A14CE4 /* ProfileView.swift */,
0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */, 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */,
0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */, 0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */,
@ -1389,6 +1392,7 @@
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */, 0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */, 0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */, 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */,
0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */, 0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */,
0E04F0092883466500BFCE1C /* DefaultLightUtils.swift in Sources */, 0E04F0092883466500BFCE1C /* DefaultLightUtils.swift in Sources */,
0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */, 0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */,
@ -1415,7 +1419,7 @@
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */, 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */, 0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */,
0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */, 0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */,
0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */, 0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */,
0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */, 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */,
0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */, 0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */,
0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */, 0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */,

View File

@ -41,8 +41,6 @@ struct AccountView: View {
@State private var liveAccount = Profile.Account() @State private var liveAccount = Profile.Account()
@State private var isPasswordRevealed = false
init( init(
providerName: ProviderName?, providerName: ProviderName?,
vpnProtocol: VPNProtocolType, vpnProtocol: VPNProtocolType,
@ -60,6 +58,13 @@ struct AccountView: View {
var body: some View { var body: some View {
List { List {
Section {
themeTextPicker(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption, selection: $liveAccount.authenticationMethod ?? .persistent, values: [
.persistent,
.interactive
// .totp // TODO: interactive, support OTP-based authentication
], description: \.localizedDescription)
}
Section { Section {
TextField(usernamePlaceholder ?? L10n.Account.Items.Username.placeholder, text: $liveAccount.username) TextField(usernamePlaceholder ?? L10n.Account.Items.Username.placeholder, text: $liveAccount.username)
.textContentType(.username) .textContentType(.username)
@ -67,15 +72,34 @@ struct AccountView: View {
.themeRawTextStyle() .themeRawTextStyle()
.withLeadingText(L10n.Account.Items.Username.caption) .withLeadingText(L10n.Account.Items.Username.caption)
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password) { switch liveAccount.authenticationMethod {
themeConceilImage.asSystemImage case nil, .persistent, .interactive:
.themeAccentForegroundStyle() if liveAccount.authenticationMethod == .interactive {
} revealImage: { EmptyView()
themeRevealImage.asSystemImage } else {
.themeAccentForegroundStyle() RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password) {
}.textContentType(.password) themeConceilImage.asSystemImage
.themeRawTextStyle() .themeAccentForegroundStyle()
.withLeadingText(L10n.Account.Items.Password.caption) } revealImage: {
themeRevealImage.asSystemImage
.themeAccentForegroundStyle()
}.textContentType(.password)
.themeRawTextStyle()
.withLeadingText(L10n.Account.Items.Password.caption)
}
// TODO: interactive, scan QR code
case .totp:
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password) {
themeConceilImage.asSystemImage
.themeAccentForegroundStyle()
} revealImage: {
themeRevealImage.asSystemImage
.themeAccentForegroundStyle()
}.textContentType(.oneTimeCode)
.themeRawTextStyle()
.withLeadingText(L10n.Account.Items.Seed.caption)
}
} footer: { } footer: {
metadata?.localizedGuidanceString.map { metadata?.localizedGuidanceString.map {
Text($0) Text($0)
@ -125,3 +149,18 @@ extension AccountView {
return providerManager.provider(withName: name) return providerManager.provider(withName: name)
} }
} }
private extension Profile.Account.AuthenticationMethod {
var localizedDescription: String {
switch self {
case .persistent:
return L10n.Account.Items.AuthenticationMethod.persistent
case .interactive:
return L10n.Account.Items.AuthenticationMethod.interactive
case .totp:
return Unlocalized.Other.totp
}
}
}

View File

@ -0,0 +1,81 @@
//
// InteractiveConnectionView.swift
// Passepartout
//
// Created by Davide De Rosa on 3/15/23.
// Copyright (c) 2022 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 SwiftUI
import PassepartoutLibrary
struct InteractiveConnectionView: View {
@Environment(\.presentationMode) private var presentationMode
@ObservedObject private var profileManager: ProfileManager
@ObservedObject private var vpnManager: VPNManager
private let profile: Profile
@State private var password = ""
init(profile: Profile) {
profileManager = .shared
vpnManager = .shared
self.profile = profile
}
var body: some View {
List {
Section {
TextField(L10n.Account.Items.Username.placeholder, text: .constant(profile.account.username))
.withLeadingText(L10n.Account.Items.Username.caption)
.disabled(true)
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $password) {
themeConceilImage.asSystemImage
.themeAccentForegroundStyle()
} revealImage: {
themeRevealImage.asSystemImage
.themeAccentForegroundStyle()
}.textContentType(.password)
.themeRawTextStyle()
.withLeadingText(L10n.Account.Items.Password.caption)
} header: {
Text(L10n.Account.title)
}
}.toolbar {
themeCloseItem(presentationMode: presentationMode)
ToolbarItem(placement: .confirmationAction) {
Button(action: saveAccount) {
Text(L10n.Global.Strings.connect)
}
}
}.navigationTitle(profile.header.name)
}
private func saveAccount() {
Task {
try? await vpnManager.connect(with: profile.id, newPassword: password)
}
presentationMode.wrappedValue.dismiss()
}
}

View File

@ -0,0 +1,79 @@
//
// OrganizerView+ProfileRow.swift
// Passepartout
//
// Created by Davide De Rosa on 4/28/22.
// Copyright (c) 2022 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 SwiftUI
import PassepartoutLibrary
extension OrganizerView {
struct ProfileRow: View {
private let profile: Profile
private let isActiveProfile: Bool
@Binding private var modalType: ModalType?
private var interactiveProfile: Binding<Profile?> {
.init {
if case .interactiveAccount(let profile) = modalType {
return profile
}
return nil
} set: {
if let profile = $0 {
modalType = .interactiveAccount(profile: profile)
} else {
modalType = nil
}
}
}
init(profile: Profile, isActiveProfile: Bool, modalType: Binding<ModalType?>) {
self.profile = profile
self.isActiveProfile = isActiveProfile
_modalType = modalType
}
var body: some View {
debugChanges()
return HStack {
VStack(alignment: .leading, spacing: 5) {
Text(profile.header.name)
.font(.headline)
.themeLongTextStyle()
VPNStatusText(isActiveProfile: isActiveProfile)
.font(.subheadline)
.themeSecondaryTextStyle()
}
Spacer()
VPNToggle(
profile: profile,
interactiveProfile: interactiveProfile,
rateLimit: Constants.RateLimit.vpnToggle
).labelsHidden()
}.padding([.top, .bottom], 10)
}
}
}

View File

@ -29,11 +29,14 @@ import PassepartoutLibrary
extension OrganizerView { extension OrganizerView {
struct ProfilesList: View { struct ProfilesList: View {
@ObservedObject private var profileManager: ProfileManager @ObservedObject private var profileManager: ProfileManager
init() {
profileManager = .shared
}
@Binding private var modalType: ModalType?
init(modalType: Binding<ModalType?>) {
profileManager = .shared
_modalType = modalType
}
var body: some View { var body: some View {
debugChanges() debugChanges()
return Group { return Group {
@ -92,7 +95,8 @@ extension OrganizerView {
private func profileLabel(forProfile profile: Profile) -> some View { private func profileLabel(forProfile profile: Profile) -> some View {
ProfileRow( ProfileRow(
profile: profile, profile: profile,
isActiveProfile: profileManager.isActiveProfile(profile.id) isActiveProfile: profileManager.isActiveProfile(profile.id),
modalType: $modalType
) )
} }

View File

@ -27,6 +27,17 @@ import SwiftUI
import PassepartoutLibrary import PassepartoutLibrary
struct OrganizerView: View { struct OrganizerView: View {
enum ModalType: Identifiable {
case interactiveAccount(profile: Profile)
// XXX: alert ids
var id: Int {
switch self {
case .interactiveAccount: return 1
}
}
}
enum AlertType: Identifiable { enum AlertType: Identifiable {
case subscribeReddit case subscribeReddit
@ -44,6 +55,8 @@ struct OrganizerView: View {
@State private var addProfileModalType: AddProfileMenu.ModalType? @State private var addProfileModalType: AddProfileMenu.ModalType?
@State private var modalType: ModalType?
@State private var alertType: AlertType? @State private var alertType: AlertType?
@State private var isHostFileImporterPresented = false @State private var isHostFileImporterPresented = false
@ -58,7 +71,7 @@ struct OrganizerView: View {
debugChanges() debugChanges()
return ZStack { return ZStack {
hiddenSceneView hiddenSceneView
ProfilesList() ProfilesList(modalType: $modalType)
}.toolbar { }.toolbar {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
AddProfileMenu( AddProfileMenu(
@ -71,7 +84,8 @@ struct OrganizerView: View {
SettingsButton() SettingsButton()
} }
} }
}.alert(item: $alertType, content: presentedAlert) }.sheet(item: $modalType, content: presentedModal)
.alert(item: $alertType, content: presentedAlert)
.fileImporter( .fileImporter(
isPresented: $isHostFileImporterPresented, isPresented: $isHostFileImporterPresented,
allowedContentTypes: hostFileTypes, allowedContentTypes: hostFileTypes,
@ -119,6 +133,16 @@ extension OrganizerView {
addProfileModalType = .addHost(url, false) addProfileModalType = .addHost(url, false)
} }
@ViewBuilder
private func presentedModal(_ modalType: ModalType) -> some View {
switch modalType {
case .interactiveAccount(let profile):
NavigationView {
InteractiveConnectionView(profile: profile)
}.themeGlobal()
}
}
private func presentedAlert(_ alertType: AlertType) -> Alert { private func presentedAlert(_ alertType: AlertType) -> Alert {
switch alertType { switch alertType {
case .subscribeReddit: case .subscribeReddit:

View File

@ -1,51 +0,0 @@
//
// ProfileRow.swift
// Passepartout
//
// Created by Davide De Rosa on 4/28/22.
// Copyright (c) 2022 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 SwiftUI
import PassepartoutLibrary
struct ProfileRow: View {
let profile: Profile
let isActiveProfile: Bool
var body: some View {
debugChanges()
return HStack {
VStack(alignment: .leading, spacing: 5) {
Text(profile.header.name)
.font(.headline)
.themeLongTextStyle()
VPNStatusText(isActiveProfile: isActiveProfile)
.font(.subheadline)
.themeSecondaryTextStyle()
}
Spacer()
VPNToggle(profileId: profile.id, rateLimit: Constants.RateLimit.vpnToggle)
.labelsHidden()
}.padding([.top, .bottom], 10)
}
}

View File

@ -30,15 +30,26 @@ extension ProfileView {
struct VPNSection: View { struct VPNSection: View {
@ObservedObject private var profileManager: ProfileManager @ObservedObject private var profileManager: ProfileManager
private let profileId: UUID private let profile: Profile
@Binding private var modalType: ModalType?
private var interactiveProfile: Binding<Profile?> {
.init {
modalType == .interactiveAccount ? profile : nil
} set: {
modalType = $0 != nil ? .interactiveAccount : nil
}
}
private var isActiveProfile: Bool { private var isActiveProfile: Bool {
profileManager.isActiveProfile(profileId) profileManager.isActiveProfile(profile.id)
} }
init(profileId: UUID) { init(profile: Profile, modalType: Binding<ModalType?>) {
profileManager = .shared profileManager = .shared
self.profileId = profileId self.profile = profile
_modalType = modalType
} }
var body: some View { var body: some View {
@ -54,7 +65,11 @@ extension ProfileView {
} }
private var toggleView: some View { private var toggleView: some View {
VPNToggle(profileId: profileId, rateLimit: Constants.RateLimit.vpnToggle) VPNToggle(
profile: profile,
interactiveProfile: interactiveProfile,
rateLimit: Constants.RateLimit.vpnToggle
)
} }
private var statusView: some View { private var statusView: some View {

View File

@ -28,6 +28,8 @@ import PassepartoutLibrary
struct ProfileView: View { struct ProfileView: View {
enum ModalType: Int, Identifiable { enum ModalType: Int, Identifiable {
case interactiveAccount
case shortcuts case shortcuts
case rename case rename
@ -89,7 +91,10 @@ struct ProfileView: View {
private var mainView: some View { private var mainView: some View {
List { List {
if !isLoading { if !isLoading {
VPNSection(profileId: currentProfile.value.id) VPNSection(
profile: currentProfile.value,
modalType: $modalType
)
ProviderSection(currentProfile: currentProfile) ProviderSection(currentProfile: currentProfile)
ConfigurationSection( ConfigurationSection(
currentProfile: currentProfile, currentProfile: currentProfile,
@ -105,6 +110,11 @@ struct ProfileView: View {
@ViewBuilder @ViewBuilder
private func presentedModal(_ modalType: ModalType) -> some View { private func presentedModal(_ modalType: ModalType) -> some View {
switch modalType { switch modalType {
case .interactiveAccount:
NavigationView {
InteractiveConnectionView(profile: currentProfile.value)
}.themeGlobal()
case .shortcuts: case .shortcuts:
NavigationView { NavigationView {
ShortcutsView(target: currentProfile.value) ShortcutsView(target: currentProfile.value)

View File

@ -35,14 +35,20 @@ struct VPNToggle: View {
@ObservedObject private var productManager: ProductManager @ObservedObject private var productManager: ProductManager
private let profileId: UUID private let profile: Profile
@Binding private var interactiveProfile: Profile?
private let rateLimit: Int private let rateLimit: Int
private var isEnabled: Binding<Bool> { private var isEnabled: Binding<Bool> {
.init { .init {
isActiveProfile && currentVPNState.isEnabled isActiveProfile && currentVPNState.isEnabled && !shouldPromptForAccount
} set: { newValue in } set: { newValue in
guard !shouldPromptForAccount else {
interactiveProfile = profile
return
}
guard newValue else { guard newValue else {
disableVPN() disableVPN()
return return
@ -52,7 +58,11 @@ struct VPNToggle: View {
} }
private var isActiveProfile: Bool { private var isActiveProfile: Bool {
profileManager.isActiveProfile(profileId) profileManager.isActiveProfile(profile.id)
}
private var shouldPromptForAccount: Bool {
profile.account.authenticationMethod == .interactive && (currentVPNState.vpnStatus == .disconnecting || currentVPNState.vpnStatus == .disconnected)
} }
private var isEligibleForSiri: Bool { private var isEligibleForSiri: Bool {
@ -61,12 +71,13 @@ struct VPNToggle: View {
@State private var canToggle = true @State private var canToggle = true
init(profileId: UUID, rateLimit: Int) { init(profile: Profile, interactiveProfile: Binding<Profile?>, rateLimit: Int) {
profileManager = .shared profileManager = .shared
vpnManager = .shared vpnManager = .shared
currentVPNState = .shared currentVPNState = .shared
productManager = .shared productManager = .shared
self.profileId = profileId self.profile = profile
_interactiveProfile = interactiveProfile
self.rateLimit = rateLimit self.rateLimit = rateLimit
} }
@ -82,10 +93,10 @@ struct VPNToggle: View {
await Task.maybeWait(forMilliseconds: rateLimit) await Task.maybeWait(forMilliseconds: rateLimit)
canToggle = true canToggle = true
do { do {
let profile = try await vpnManager.connect(with: profileId) let profile = try await vpnManager.connect(with: profile.id)
donateIntents(withProfile: profile) donateIntents(withProfile: profile)
} catch { } catch {
pp_log.warning("Unable to connect to profile \(profileId): \(error)") pp_log.warning("Unable to connect to profile \(profile.id): \(error)")
canToggle = true canToggle = true
} }
} }

View File

@ -54,6 +54,12 @@ internal enum L10n {
/// MARK: ProfileView -> AccountView /// MARK: ProfileView -> AccountView
internal static let title = L10n.tr("Localizable", "account.title", fallback: "Account") internal static let title = L10n.tr("Localizable", "account.title", fallback: "Account")
internal enum Items { internal enum Items {
internal enum AuthenticationMethod {
/// Interactive
internal static let interactive = L10n.tr("Localizable", "account.items.authentication_method.interactive", fallback: "Interactive")
/// Persistent
internal static let persistent = L10n.tr("Localizable", "account.items.authentication_method.persistent", fallback: "Persistent")
}
internal enum OpenGuide { internal enum OpenGuide {
/// See your credentials /// See your credentials
internal static let caption = L10n.tr("Localizable", "account.items.open_guide.caption", fallback: "See your credentials") internal static let caption = L10n.tr("Localizable", "account.items.open_guide.caption", fallback: "See your credentials")
@ -64,6 +70,10 @@ internal enum L10n {
/// secret /// secret
internal static let placeholder = L10n.tr("Localizable", "account.items.password.placeholder", fallback: "secret") internal static let placeholder = L10n.tr("Localizable", "account.items.password.placeholder", fallback: "secret")
} }
internal enum Seed {
/// Seed
internal static let caption = L10n.tr("Localizable", "account.items.seed.caption", fallback: "Seed")
}
internal enum Signup { internal enum Signup {
/// Register with %@ /// Register with %@
internal static func caption(_ p1: Any) -> String { internal static func caption(_ p1: Any) -> String {

View File

@ -252,5 +252,7 @@ enum Unlocalized {
enum Other { enum Other {
static let siri = "Siri" static let siri = "Siri"
static let totp = "TOTP"
} }
} }

View File

@ -179,10 +179,13 @@
"account.title" = "Account"; "account.title" = "Account";
"account.sections.credentials.header" = "Credentials"; "account.sections.credentials.header" = "Credentials";
"account.sections.registration.footer" = "Go get an account on the %@ website."; "account.sections.registration.footer" = "Go get an account on the %@ website.";
"account.items.authentication_method.persistent" = "Persistent";
"account.items.authentication_method.interactive" = "Interactive";
"account.items.username.caption" = "Username"; "account.items.username.caption" = "Username";
"account.items.username.placeholder" = "username"; "account.items.username.placeholder" = "username";
"account.items.password.caption" = "Password"; "account.items.password.caption" = "Password";
"account.items.password.placeholder" = "secret"; "account.items.password.placeholder" = "secret";
"account.items.seed.caption" = "Seed";
"account.items.open_guide.caption" = "See your credentials"; "account.items.open_guide.caption" = "See your credentials";
"account.items.signup.caption" = "Register with %@"; "account.items.signup.caption" = "Register with %@";

View File

@ -27,6 +27,16 @@ import Foundation
extension Profile { extension Profile {
public struct Account: Codable, Equatable { public struct Account: Codable, Equatable {
public enum AuthenticationMethod: String, Codable {
case persistent
case interactive
case totp
}
public var authenticationMethod: AuthenticationMethod?
public var username: String public var username: String
public var password: String public var password: String

View File

@ -42,8 +42,17 @@ extension NEOnDemandRuleInterfaceType {
} }
} }
extension Profile.OnDemand { extension Profile {
func rules(withCustomRules: Bool) -> [NEOnDemandRule] { func onDemandRules(withCustomRules: Bool) -> [NEOnDemandRule] {
onDemand.rules(isInteractive: account.authenticationMethod == .interactive, withCustomRules: withCustomRules)
}
}
private extension Profile.OnDemand {
func rules(isInteractive: Bool, withCustomRules: Bool) -> [NEOnDemandRule] {
guard isEnabled && !isInteractive else {
return []
}
// TODO: on-demand, drop hardcoding when "trusted networks" -> "on-demand" // TODO: on-demand, drop hardcoding when "trusted networks" -> "on-demand"
// isEnabled = true // isEnabled = true

View File

@ -49,9 +49,9 @@ extension VPNManager {
} }
@discardableResult @discardableResult
public func connect(with profileId: UUID) async throws -> Profile { public func connect(with profileId: UUID, newPassword: String? = nil) async throws -> Profile {
let result = try profileManager.liveProfileEx(withId: profileId) let result = try profileManager.liveProfileEx(withId: profileId)
let profile = result.profile var profile = result.profile
guard !profileManager.isActiveProfile(profileId) || guard !profileManager.isActiveProfile(profileId) ||
currentState.vpnStatus != .connected else { currentState.vpnStatus != .connected else {
@ -63,6 +63,9 @@ extension VPNManager {
} }
pp_log.info("Connecting to: \(profile.logDescription)") pp_log.info("Connecting to: \(profile.logDescription)")
if let newPassword {
profile.account.password = newPassword
}
let cfg = try vpnConfiguration(withProfile: profile) let cfg = try vpnConfiguration(withProfile: profile)
profileManager.activateProfile(profile) profileManager.activateProfile(profile)

View File

@ -60,7 +60,7 @@ struct VPNConfigurationParameters {
username = !profile.account.username.isEmpty ? profile.account.username : nil username = !profile.account.username.isEmpty ? profile.account.username : nil
self.passwordReference = passwordReference self.passwordReference = passwordReference
self.withNetworkSettings = withNetworkSettings self.withNetworkSettings = withNetworkSettings
onDemandRules = profile.onDemand.rules(withCustomRules: withCustomRules) onDemandRules = profile.onDemandRules(withCustomRules: withCustomRules)
} }
} }