summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadSettings/AccessMethodRepository.swift
blob: 2df3ce7c893e53ba1eb375ea6cc5b7d56a29674d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//
//  AccessMethodRepository.swift
//  MullvadVPN
//
//  Created by Jon Petersson on 12/12/2023.
//  Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Combine
import Foundation
import MullvadLogging

public class AccessMethodRepository: AccessMethodRepositoryProtocol {
    private let logger = Logger(label: "AccessMethodRepository")

    private let direct = PersistentAccessMethod(
        id: UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")!,
        name: "Direct",
        isEnabled: true,
        proxyConfiguration: .direct
    )

    private let bridge = PersistentAccessMethod(
        id: UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")!,
        name: "Mullvad bridges",
        isEnabled: true,
        proxyConfiguration: .bridges
    )

    private let accessMethodsSubject: CurrentValueSubject<[PersistentAccessMethod], Never>
    public var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> {
        accessMethodsSubject.eraseToAnyPublisher()
    }

    private let lastReachableAccessMethodSubject: CurrentValueSubject<PersistentAccessMethod, Never>
    public var lastReachableAccessMethodPublisher: AnyPublisher<PersistentAccessMethod, Never> {
        lastReachableAccessMethodSubject.eraseToAnyPublisher()
    }

    public var directAccess: PersistentAccessMethod {
        direct
    }

    public init() {
        accessMethodsSubject = CurrentValueSubject([])
        lastReachableAccessMethodSubject = CurrentValueSubject(direct)

        add([direct, bridge])

        accessMethodsSubject.send(fetchAll())
        lastReachableAccessMethodSubject.send(fetchLastReachable())
    }

    public func save(_ method: PersistentAccessMethod) {
        var methodStore = readApiAccessMethodStore()

        if let index = methodStore.accessMethods.firstIndex(where: { $0.id == method.id }) {
            methodStore.accessMethods[index] = method
        } else {
            methodStore.accessMethods.append(method)
        }

        do {
            try writeApiAccessMethodStore(methodStore)
            accessMethodsSubject.send(methodStore.accessMethods)
        } catch {
            logger.error("Could not save access method: \(method) \nError: \(error)")
        }
    }

    public func saveLastReachable(_ method: PersistentAccessMethod) {
        var methodStore = readApiAccessMethodStore()
        methodStore.lastReachableAccessMethod = method

        do {
            try writeApiAccessMethodStore(methodStore)
            lastReachableAccessMethodSubject.send(method)
        } catch {
            logger.error("Could not save last reachable access method: \(method) \nError: \(error)")
        }
    }

    public func delete(id: UUID) {
        var methodStore = readApiAccessMethodStore()
        guard let index = methodStore.accessMethods.firstIndex(where: { $0.id == id }) else { return }

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

        do {
            try writeApiAccessMethodStore(methodStore)
            accessMethodsSubject.send(methodStore.accessMethods)
        } catch {
            logger.error("Could not delete access method with id: \(id) \nError: \(error)")
        }
    }

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

    public func fetchAll() -> [PersistentAccessMethod] {
        readApiAccessMethodStore().accessMethods
    }

    public func fetchLastReachable() -> PersistentAccessMethod {
        readApiAccessMethodStore().lastReachableAccessMethod
    }

    public func reloadWithDefaultsAfterDataRemoval() {
        add([direct, bridge])
    }

    private func add(_ methods: [PersistentAccessMethod]) {
        var methodStore = readApiAccessMethodStore()

        methods.forEach { method in
            if !methodStore.accessMethods.contains(where: { $0.id == method.id }) {
                methodStore.accessMethods.append(method)
            }
        }

        do {
            try writeApiAccessMethodStore(methodStore)
            accessMethodsSubject.send(methods)
        } catch {
            logger.error("Could not update access methods: \(methods) \nError: \(error)")
        }
    }

    private func readApiAccessMethodStore() -> PersistentAccessMethodStore {
        let parser = makeParser()

        do {
            let data = try SettingsManager.store.read(key: .apiAccessMethods)
            return try parser.parseUnversionedPayload(as: PersistentAccessMethodStore.self, from: data)
        } catch {
            logger.error("Could not load access method store: \(error)")
            return PersistentAccessMethodStore(lastReachableAccessMethod: direct, accessMethods: [])
        }
    }

    private func writeApiAccessMethodStore(_ store: PersistentAccessMethodStore) throws {
        let parser = makeParser()
        let data = try parser.produceUnversionedPayload(store)

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

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