Skip to content

Commit

Permalink
Add API access methods UI/part of backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrej Mihajlov committed Nov 30, 2023
1 parent 5c8d672 commit 882a253
Show file tree
Hide file tree
Showing 97 changed files with 6,604 additions and 256 deletions.
480 changes: 451 additions & 29 deletions ios/MullvadVPN.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions ios/MullvadVPN/AccessMethodRepository/AccessMethodRepository.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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]
}
124 changes: 124 additions & 0 deletions ios/MullvadVPN/AccessMethodRepository/PersistentAccessMethod.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
48 changes: 48 additions & 0 deletions ios/MullvadVPN/AccessMethodRepository/ShadowsocksCipher.swift
Original file line number Diff line number Diff line change
@@ -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",
]
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import UIKit

/// Custom navigation controller that applies the custom appearance to itself.
class CustomNavigationController: UINavigationController {
override var childForStatusBarHidden: UIViewController? {
topViewController
Expand All @@ -22,4 +23,11 @@ class CustomNavigationController: UINavigationController {

navigationBar.configureCustomAppeareance()
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

// Navigation bar updates the prompt color on layout so we have to force our own appearance on each layout pass.
navigationBar.overridePromptColor()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
import UIKit

extension UINavigationBar {
/// Locates the navigation bar prompt label within the view hirarchy and overrides the text color.
/// - Note: Navigation bar does not provide the appearance configuration for the prompt.
func overridePromptColor() {
let promptView = subviews.first { $0.description.contains("Prompt") }
let promptLabel = promptView?.subviews.first { $0 is UILabel } as? UILabel

promptLabel?.textColor = UIColor.NavigationBar.promptColor
}

func configureCustomAppeareance() {
var directionalMargins = directionalLayoutMargins
directionalMargins.leading = UIMetrics.contentLayoutMargins.leading
Expand Down
Loading

0 comments on commit 882a253

Please sign in to comment.