Skip to content

Commit

Permalink
Store API access method settings
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Dec 13, 2023
1 parent a8de614 commit 9ac4c68
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 62 deletions.
36 changes: 7 additions & 29 deletions ios/MullvadSettings/SettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public enum SettingsManager {
)

/// Alternative store used for tests.
internal static var unitTestStore: SettingsStore?
public static var unitTestStore: SettingsStore?

public static var store: SettingsStore {
if let unitTestStore { return unitTestStore }
Expand Down Expand Up @@ -140,41 +140,19 @@ public enum SettingsManager {
try store.write(data, for: .deviceState)
}

/// Removes all legacy settings, device state and tunnel settings but keeps the last used
/// account number stored.
/// Removes all legacy settings, device state, tunnel settings and API access methods but keeps
/// the last used account number stored.
public static func resetStore(completely: Bool = false) {
logger.debug("Reset store.")

do {
try store.delete(key: .deviceState)
} catch {
if (error as? KeychainError) != .itemNotFound {
logger.error(error: error, message: "Failed to delete device state.")
}
}

do {
try store.delete(key: .settings)
} catch {
if (error as? KeychainError) != .itemNotFound {
logger.error(error: error, message: "Failed to delete settings.")
}
}

if completely {
do {
try store.delete(key: .lastUsedAccount)
} catch {
if (error as? KeychainError) != .itemNotFound {
logger.error(error: error, message: "Failed to delete last used account.")
}
}
let keys = completely ? SettingsKey.allCases : [.settings, .deviceState, .apiAccessMethods]

keys.forEach { key in
do {
try store.delete(key: .shouldWipeSettings)
try store.delete(key: key)
} catch {
if (error as? KeychainError) != .itemNotFound {
logger.error(error: error, message: "Failed to delete should wipe settings.")
logger.error(error: error, message: "Failed to delete \(key.rawValue).")
}
}
}
Expand Down
1 change: 1 addition & 0 deletions ios/MullvadSettings/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Foundation
public enum SettingsKey: String, CaseIterable {
case settings = "Settings"
case deviceState = "DeviceState"
case apiAccessMethods = "ApiAccessMethods"
case lastUsedAccount = "LastUsedAccount"
case shouldWipeSettings = "ShouldWipeSettings"
}
Expand Down
15 changes: 13 additions & 2 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@
5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */; };
584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26C3270C855A004EA533 /* PreferencesDataSource.swift */; };
584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */; };
584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */; };
5859A55529CD9DD900F66591 /* changes.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5859A55429CD9DD800F66591 /* changes.txt */; };
585A02E92A4B283000C6CAFF /* TCPUnsafeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02E82A4B283000C6CAFF /* TCPUnsafeListener.swift */; };
585A02EB2A4B285800C6CAFF /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */; };
Expand Down Expand Up @@ -517,6 +516,11 @@
7A6F2FAF2AFE36E7006D0856 /* PreferencesInfoButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FAE2AFE36E7006D0856 /* PreferencesInfoButtonItem.swift */; };
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */; };
7A83A0C72B29A831008B5CE7 /* AccessMethodRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */; };
7A83A0C82B29A851008B5CE7 /* PersistentAccessMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */; };
7A83A0C92B29AA8C008B5CE7 /* AccessMethodRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */; };
7A83A0CA2B29AAB5008B5CE7 /* ShadowsocksCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DFF7D92B02862E00F864E0 /* ShadowsocksCipher.swift */; };
7A83C3FF2A55B72E00DFB83A /* MullvadVPNApp.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 7A83C3FE2A55B72E00DFB83A /* MullvadVPNApp.xctestplan */; };
7A83C4022A57FAA800DFB83A /* SettingsDNSInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A83C4012A57FAA800DFB83A /* SettingsDNSInfoCell.swift */; };
7A88DCD82A8FABBE00D2FF0E /* Routing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; };
Expand Down Expand Up @@ -1651,6 +1655,7 @@
7A6F2FAE2AFE36E7006D0856 /* PreferencesInfoButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesInfoButtonItem.swift; sourceTree = "<group>"; };
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAccessMethodsTests.swift; sourceTree = "<group>"; };
7A83C3FE2A55B72E00DFB83A /* MullvadVPNApp.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNApp.xctestplan; sourceTree = "<group>"; };
7A83C4002A55B81A00DFB83A /* MullvadVPNCI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MullvadVPNCI.xctestplan; sourceTree = "<group>"; };
7A83C4012A57FAA800DFB83A /* SettingsDNSInfoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDNSInfoCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2676,6 +2681,7 @@
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */,
A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */,
A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */,
7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */,
A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */,
A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */,
5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */,
Expand All @@ -2689,7 +2695,6 @@
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */,
A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */,
58C3FA652A38549D006A450A /* MockFileCache.swift */,
F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */,
F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */,
F09D04B62AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift */,
F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */,
Expand All @@ -2702,6 +2707,7 @@
A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */,
A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */,
A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */,
F09D04BA2AE95396003D4F89 /* URLSessionStub.swift */,
58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */,
F0B0E6962AFE6E7E001DC66B /* XCTest+Async.swift */,
);
Expand Down Expand Up @@ -4359,6 +4365,7 @@
A9A5F9E12ACB05160083449F /* AddressCacheTracker.swift in Sources */,
A900E9BC2ACC609200C95F67 /* DevicesProxy+Stubs.swift in Sources */,
A9A5F9E22ACB05160083449F /* BackgroundTask.swift in Sources */,
7A83A0C92B29AA8C008B5CE7 /* AccessMethodRepositoryProtocol.swift in Sources */,
A9A5F9E32ACB05160083449F /* AccountDataThrottling.swift in Sources */,
A9A5F9E42ACB05160083449F /* AppPreferences.swift in Sources */,
A9A5F9E52ACB05160083449F /* CustomDateComponentsFormatting.swift in Sources */,
Expand All @@ -4380,6 +4387,7 @@
A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */,
A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */,
A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */,
7A83A0CA2B29AAB5008B5CE7 /* ShadowsocksCipher.swift in Sources */,
F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */,
F09D04B92AE95111003D4F89 /* OutgoingConnectionProxy.swift in Sources */,
A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */,
Expand All @@ -4399,6 +4407,7 @@
A9A5FA022ACB05160083449F /* RelayCacheTracker.swift in Sources */,
A9A5FA032ACB05160083449F /* SimulatorTunnelInfo.swift in Sources */,
A9A5FA042ACB05160083449F /* SimulatorTunnelProvider.swift in Sources */,
7A83A0C72B29A831008B5CE7 /* AccessMethodRepository.swift in Sources */,
A9A5FA052ACB05160083449F /* SimulatorTunnelProviderHost.swift in Sources */,
A900E9C02ACC661900C95F67 /* AccessTokenManager+Stubs.swift in Sources */,
A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */,
Expand All @@ -4424,6 +4433,7 @@
F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */,
58BE4B9D2B18A85B007EA1D3 /* NSAttributedString+Markdown.swift in Sources */,
A9A5FA172ACB05160083449F /* SendTunnelProviderMessageOperation.swift in Sources */,
7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */,
A9A5FA182ACB05160083449F /* SetAccountOperation.swift in Sources */,
A9A5FA192ACB05160083449F /* StartTunnelOperation.swift in Sources */,
A9A5FA1A2ACB05160083449F /* StopTunnelOperation.swift in Sources */,
Expand Down Expand Up @@ -4457,6 +4467,7 @@
A9A5FA302ACB05160083449F /* InputTextFormatterTests.swift in Sources */,
F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */,
A9A5FA312ACB05160083449F /* MockFileCache.swift in Sources */,
7A83A0C82B29A851008B5CE7 /* PersistentAccessMethod.swift in Sources */,
A9A5FA322ACB05160083449F /* RelayCacheTests.swift in Sources */,
A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */,
58DFF7D32B02570000F864E0 /* MarkdownStylingOptions.swift in Sources */,
Expand Down
109 changes: 78 additions & 31 deletions ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,115 @@
// AccessMethodRepository.swift
// MullvadVPN
//
// Created by pronebird on 22/11/2023.
// Created by Jon Petersson on 12/12/2023.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Combine
import Foundation
import MullvadSettings

class AccessMethodRepository: AccessMethodRepositoryProtocol {
private var memoryStore: [PersistentAccessMethod] {
didSet {
publisher.send(memoryStore)
}
}

let publisher: PassthroughSubject<[PersistentAccessMethod], Never> = .init()

static let shared = AccessMethodRepository()

private var defaultDirectMethod: PersistentAccessMethod {
PersistentAccessMethod(
id: UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")!,
name: "",
isEnabled: true,
proxyConfiguration: .direct
)
}

private var defaultBridgesMethod: PersistentAccessMethod {
PersistentAccessMethod(
id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")!,
name: "",
isEnabled: true,
proxyConfiguration: .bridges
)
}

init() {
memoryStore = [
PersistentAccessMethod(
id: UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")!,
name: "",
isEnabled: true,
proxyConfiguration: .direct
),
PersistentAccessMethod(
id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")!,
name: "",
isEnabled: true,
proxyConfiguration: .bridges
),
]
add([defaultDirectMethod, defaultBridgesMethod])
}

func add(_ method: PersistentAccessMethod) {
guard !memoryStore.contains(where: { $0.id == method.id }) else { return }
add([method])
}

func add(_ methods: [PersistentAccessMethod]) {
var storedMethods = fetchAll()

methods.forEach { method in
guard !storedMethods.contains(where: { $0.id == method.id }) else { return }
storedMethods.append(method)
}

memoryStore.append(method)
do {
try writeApiAccessMethods(storedMethods)
} catch {
print("Could not add access method(s): \(methods) \nError: \(error)")
}
}

func update(_ method: PersistentAccessMethod) {
guard let index = memoryStore.firstIndex(where: { $0.id == method.id }) else { return }
var methods = fetchAll()

guard let index = methods.firstIndex(where: { $0.id == method.id }) else { return }
methods[index] = method

memoryStore[index] = method
do {
try writeApiAccessMethods(methods)
} catch {
print("Could not update access method: \(method) \nError: \(error)")
}
}

func delete(id: UUID) {
guard let index = memoryStore.firstIndex(where: { $0.id == id }) else { return }
var methods = fetchAll()
guard let index = methods.firstIndex(where: { $0.id == id }) else { return }

// Prevent removing methods that have static UUIDs and always present.
let permanentMethod = memoryStore[index]
// Prevent removing methods that have static UUIDs and are always present.
let permanentMethod = methods[index]
if !permanentMethod.kind.isPermanent {
memoryStore.remove(at: index)
methods.remove(at: index)
}

do {
try writeApiAccessMethods(methods)
} catch {
print("Could not delete access method with id: \(id) \nError: \(error)")
}
}

func fetch(by id: UUID) -> PersistentAccessMethod? {
memoryStore.first { $0.id == id }
fetchAll().first { $0.id == id }
}

func fetchAll() -> [PersistentAccessMethod] {
memoryStore
(try? readApiAccessMethods()) ?? []
}

private func readApiAccessMethods() throws -> [PersistentAccessMethod] {
let parser = makeParser()
let data = try SettingsManager.store.read(key: .apiAccessMethods)

return try parser.parseUnversionedPayload(as: [PersistentAccessMethod].self, from: data)
}

private func writeApiAccessMethods(_ accessMethods: [PersistentAccessMethod]) throws {
let parser = makeParser()
let data = try parser.produceUnversionedPayload(accessMethods)

try SettingsManager.store.write(data, for: .apiAccessMethods)

publisher.send(accessMethods)
}

private func makeParser() -> SettingsParser {
SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
}
}
Loading

0 comments on commit 9ac4c68

Please sign in to comment.