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
|
//
// AddressCache.swift
// MullvadREST
//
// Created by pronebird on 08/12/2021.
// Copyright © 2021 Mullvad VPN AB. All rights reserved.
//
import Foundation
import MullvadLogging
import MullvadTypes
extension REST {
public final class AddressCache {
/// Logger.
private let logger = Logger(label: "AddressCache")
/// Disk cache.
private let fileCache: any FileCacheProtocol<CachedAddresses>
/// Memory cache.
private var cache: CachedAddresses = defaultCachedAddresses
/// Lock used for synchronizing access to instance members.
private let cacheLock = NSLock()
/// Whether address cache can be written to.
private let canWriteToCache: Bool
/// The default set of endpoints to use as a fallback mechanism
private static let defaultCachedAddresses = CachedAddresses(
updatedAt: Date(timeIntervalSince1970: 0),
endpoints: [REST.defaultAPIEndpoint]
)
// MARK: - Public API
/// Designated initializer.
public init(canWriteToCache: Bool, cacheDirectory: URL) {
fileCache = FileCache(
fileURL: cacheDirectory.appendingPathComponent("api-ip-address.json", isDirectory: false)
)
self.canWriteToCache = canWriteToCache
}
/// Initializer that accepts a file cache implementation and can be used in tests.
init(canWriteToCache: Bool, fileCache: some FileCacheProtocol<CachedAddresses>) {
self.fileCache = fileCache
self.canWriteToCache = canWriteToCache
}
/// Returns the latest available endpoint
///
/// When running from the Network Extension, this method will read from the cache before returning.
/// - Returns: The latest available endpoint, or a default endpoint if no endpoints are available
public func getCurrentEndpoint() -> AnyIPEndpoint {
cacheLock.lock()
defer { cacheLock.unlock() }
var currentEndpoint = cache.endpoints.first ?? REST.defaultAPIEndpoint
// Reload from disk cache when in the Network Extension as there is no `AddressCacheTracker` running
// there
if canWriteToCache == false {
do {
cache = try fileCache.read()
if let firstEndpoint = cache.endpoints.first {
currentEndpoint = firstEndpoint
}
} catch {
logger.error(error: error, message: "Failed to read address cache from disk.")
}
}
return currentEndpoint
}
/// Updates the available endpoints to use
///
/// Only the first available endpoint is kept, the rest are discarded.
/// This method will only modify the on disk cache when running from the UI process.
/// - Parameter endpoints: The new endpoints to use for API requests
public func setEndpoints(_ endpoints: [AnyIPEndpoint]) {
cacheLock.lock()
defer { cacheLock.unlock() }
guard let firstEndpoint = endpoints.first else { return }
if Set(cache.endpoints) == Set(endpoints) {
cache.updatedAt = Date()
} else {
cache = CachedAddresses(
updatedAt: Date(),
endpoints: [firstEndpoint]
)
}
guard canWriteToCache else { return }
do {
try fileCache.write(cache)
} catch {
logger.error(
error: error,
message: "Failed to write address cache after setting new endpoints."
)
}
}
/// The `Date` when the cache was last updated at
///
/// - Returns: The `Date` when the cache was last updated at
public func getLastUpdateDate() -> Date {
cacheLock.lock()
defer { cacheLock.unlock() }
return cache.updatedAt
}
/// Initializes the cache by reading the a cached file on disk.
///
/// If no cache file is present, a default API endpoint will be selected instead.
public func loadFromFile() {
// The first time the application is ran, this statement will fail as there is no cache. This is fine.
// The cache will be filled when either `getCurrentEndpoint` or `setEndpoints()` are called.
do {
cache = try fileCache.read()
} catch {
logger.debug("Initialized cache with default API endpoint.")
cache = Self.defaultCachedAddresses
}
}
}
public struct CachedAddresses: Codable, Equatable {
/// Date when the cached addresses were last updated.
var updatedAt: Date
/// API endpoints.
var endpoints: [AnyIPEndpoint]
}
}
|