diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-11-02 10:47:15 +0100 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@kvadrat.se> | 2023-12-13 15:42:20 +0100 |
| commit | 8188c51e1b0a5705db7e94053c6d8d45faf615fe (patch) | |
| tree | 79b38d2b0ff8d8d2fd5b7ab06adde476c3c523e5 /ios/MullvadVPN/AccessMethodRepository | |
| parent | c1e2a227931e8e6011353ff9fce56fe733e4ac63 (diff) | |
| download | mullvadvpn-8188c51e1b0a5705db7e94053c6d8d45faf615fe.tar.xz mullvadvpn-8188c51e1b0a5705db7e94053c6d8d45faf615fe.zip | |
Add API access methods UI/part of backend
Diffstat (limited to 'ios/MullvadVPN/AccessMethodRepository')
6 files changed, 335 insertions, 0 deletions
diff --git a/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift new file mode 100644 index 0000000000..c0c0ec8607 --- /dev/null +++ b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift @@ -0,0 +1,69 @@ +// +// AccessMethodRepository.swift +// MullvadVPN +// +// Created by pronebird on 22/11/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Combine +import Foundation + +class AccessMethodRepository: AccessMethodRepositoryProtocol { + private var memoryStore: [PersistentAccessMethod] { + didSet { + publisher.send(memoryStore) + } + } + + let publisher: PassthroughSubject<[PersistentAccessMethod], Never> = .init() + + static let shared = AccessMethodRepository() + + 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 + ), + ] + } + + func add(_ method: PersistentAccessMethod) { + guard !memoryStore.contains(where: { $0.id == method.id }) else { return } + + memoryStore.append(method) + } + + func update(_ method: PersistentAccessMethod) { + guard let index = memoryStore.firstIndex(where: { $0.id == method.id }) else { return } + + memoryStore[index] = method + } + + func delete(id: UUID) { + guard let index = memoryStore.firstIndex(where: { $0.id == id }) else { return } + + // Prevent removing methods that have static UUIDs and always present. + let permanentMethod = memoryStore[index] + if !permanentMethod.kind.isPermanent { + memoryStore.remove(at: index) + } + } + + func fetch(by id: UUID) -> PersistentAccessMethod? { + memoryStore.first { $0.id == id } + } + + func fetchAll() -> [PersistentAccessMethod] { + memoryStore + } +} diff --git a/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepositoryProtocol.swift b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepositoryProtocol.swift new file mode 100644 index 0000000000..213f524bcc --- /dev/null +++ b/ios/MullvadVPN/AccessMethodRepository/AccessMethodRepositoryProtocol.swift @@ -0,0 +1,36 @@ +// +// AccessMethodRepositoryProtocol.swift +// MullvadVPN +// +// Created by pronebird on 28/11/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Combine +import Foundation + +protocol AccessMethodRepositoryProtocol { + /// Publisher that propagates a snapshot of persistent store upon modifications. + var publisher: PassthroughSubject<[PersistentAccessMethod], Never> { get } + + /// Add new access method. + /// - Parameter method: persistent access method model. + func add(_ method: PersistentAccessMethod) + + /// Persist modified access method locating existing entry by id. + /// - Parameter method: persistent access method model. + func update(_ method: PersistentAccessMethod) + + /// Delete access method by id. + /// - Parameter id: an access method id. + func delete(id: UUID) + + /// Fetch access method by id. + /// - Parameter id: an access method id. + /// - Returns: a persistent access method model upon success, otherwise `nil`. + func fetch(by id: UUID) -> PersistentAccessMethod? + + /// Fetch all access method from the persistent store. + /// - Returns: an array of all persistent access method. + func fetchAll() -> [PersistentAccessMethod] +} diff --git a/ios/MullvadVPN/AccessMethodRepository/PersistentAccessMethod.swift b/ios/MullvadVPN/AccessMethodRepository/PersistentAccessMethod.swift new file mode 100644 index 0000000000..b5d5ef2947 --- /dev/null +++ b/ios/MullvadVPN/AccessMethodRepository/PersistentAccessMethod.swift @@ -0,0 +1,124 @@ +// +// PersistentAccessMethod.swift +// MullvadVPN +// +// Created by pronebird on 15/11/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes +import Network + +/// Persistent access method model. +struct PersistentAccessMethod: Identifiable, Codable { + /// The unique identifier used for referencing the access method entry in a persistent store. + var id: UUID + + /// The user-defined name for access method. + var name: String + + /// The flag indicating whether configuration is enabled. + var isEnabled: Bool + + /// Proxy configuration. + var proxyConfiguration: PersistentProxyConfiguration +} + +/// Persistent proxy configuration. +enum PersistentProxyConfiguration: Codable { + /// Direct communication without proxy. + case direct + + /// Communication over bridges. + case bridges + + /// Communication over shadowsocks. + case shadowsocks(ShadowsocksConfiguration) + + /// Communication over socks5 proxy. + case socks5(SocksConfiguration) +} + +extension PersistentProxyConfiguration { + /// Socks autentication method. + enum SocksAuthentication: Codable { + case noAuthentication + case usernamePassword(username: String, password: String) + } + + /// Socks v5 proxy configuration. + struct SocksConfiguration: Codable { + /// Proxy server address. + var server: AnyIPAddress + + /// Proxy server port. + var port: UInt16 + + /// Authentication method. + var authentication: SocksAuthentication + } + + /// Shadowsocks configuration. + struct ShadowsocksConfiguration: Codable { + /// Server address. + var server: AnyIPAddress + + /// Server port. + var port: UInt16 + + /// Server password. + var password: String + + /// Server cipher. + var cipher: ShadowsocksCipher + } +} + +extension PersistentAccessMethod { + /// A kind of access method. + var kind: AccessMethodKind { + switch proxyConfiguration { + case .direct: + .direct + case .bridges: + .bridges + case .shadowsocks: + .shadowsocks + case .socks5: + .socks5 + } + } +} + +/// A kind of API access method. +enum AccessMethodKind: Equatable, Hashable, CaseIterable { + /// Direct communication. + case direct + + /// Communication over bridges. + case bridges + + /// Communication over shadowsocks. + case shadowsocks + + /// Communication over socks v5 proxy. + case socks5 +} + +extension AccessMethodKind { + /// Returns `true` if the method is permanent and cannot be deleted. + var isPermanent: Bool { + switch self { + case .direct, .bridges: + true + case .shadowsocks, .socks5: + false + } + } + + /// Returns all access method kinds that can be added by user. + static var allUserDefinedKinds: [AccessMethodKind] { + allCases.filter { !$0.isPermanent } + } +} diff --git a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift new file mode 100644 index 0000000000..bf9ad5f03a --- /dev/null +++ b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift @@ -0,0 +1,37 @@ +// +// ProxyConfigurationTester.swift +// MullvadVPN +// +// Created by pronebird on 28/11/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Combine +import Foundation + +/// A concrete implementation of an access method proxy configuration. +class ProxyConfigurationTester: ProxyConfigurationTesterProtocol { + private var cancellable: Cancellable? + + static let shared = ProxyConfigurationTester() + + init() {} + + func start(configuration: PersistentProxyConfiguration, completion: @escaping (Error?) -> Void) { + let workItem = DispatchWorkItem { + let randomResult = (0 ... 255).randomElement()?.isMultiple(of: 2) ?? true + + completion(randomResult ? nil : URLError(.timedOut)) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: workItem) + + cancellable = AnyCancellable { + workItem.cancel() + } + } + + func cancel() { + cancellable = nil + } +} diff --git a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift new file mode 100644 index 0000000000..3ee795cbf9 --- /dev/null +++ b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift @@ -0,0 +1,21 @@ +// +// ProxyConfigurationTesterProtocol.swift +// MullvadVPN +// +// Created by pronebird on 28/11/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Type implementing access method proxy configuration testing. +protocol ProxyConfigurationTesterProtocol { + /// Start testing proxy configuration. + /// - Parameters: + /// - configuration: a proxy configuration. + /// - completion: a completion handler that receives `nil` upon success, otherwise the underlying error. + func start(configuration: PersistentProxyConfiguration, completion: @escaping (Error?) -> Void) + + /// Cancel testing proxy configuration. + func cancel() +} diff --git a/ios/MullvadVPN/AccessMethodRepository/ShadowsocksCipher.swift b/ios/MullvadVPN/AccessMethodRepository/ShadowsocksCipher.swift new file mode 100644 index 0000000000..8610bf33c1 --- /dev/null +++ b/ios/MullvadVPN/AccessMethodRepository/ShadowsocksCipher.swift @@ -0,0 +1,48 @@ +// +// ShadowsocksCipher.swift +// MullvadVPN +// +// Created by pronebird on 13/11/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Type representing a shadowsocks cipher. +struct ShadowsocksCipher: RawRepresentable, CustomStringConvertible, Equatable, Hashable, Codable { + let rawValue: String + + var description: String { + rawValue + } + + /// Default cipher. + static let `default` = ShadowsocksCipher(rawValue: "chacha20") + + /// All supported ciphers. + static let supportedCiphers = supportedCipherIdentifiers.map { ShadowsocksCipher(rawValue: $0) } +} + +private let supportedCipherIdentifiers = [ + // Stream ciphers. + "aes-128-cfb", + "aes-128-cfb1", + "aes-128-cfb8", + "aes-128-cfb128", + "aes-256-cfb", + "aes-256-cfb1", + "aes-256-cfb8", + "aes-256-cfb128", + "rc4", + "rc4-md5", + "chacha20", + "salsa20", + "chacha20-ietf", + // AEAD ciphers. + "aes-128-gcm", + "aes-256-gcm", + "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305", + "aes-128-pmac-siv", + "aes-256-pmac-siv", +] |
