Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion ios/MullvadREST/MullvadAPI/MullvadApiNetworkOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ extension REST {
logger.debug("\(#function): \(request.name) API response received")

if let apiError = response.error {
logger.error("Request failed to send error=\(apiError)")
logger
.error(
"Response contained error code \(apiError.statusCode), error: \(apiError.errorDescription)"
)
finish(result: .failure(restError(apiError: apiError)))
return
}
Expand Down
7 changes: 7 additions & 0 deletions ios/MullvadRustRuntime/MullvadAddressCacheKeychainStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@

import MullvadSettings

/// Whether the settings store is available. It requires `ApplicationSecurityGroupIdentifier`
/// to be present in the main bundle's Info.plist, which is not the case in e.g. UI test runners.
private let isSettingsStoreAvailable: Bool = Bundle.main
.object(forInfoDictionaryKey: "ApplicationSecurityGroupIdentifier") != nil

/// Store the address cache, given to us by the Rust code, to the keychain
@_cdecl("swift_store_address_cache")
func storeAddressCache(_ pointer: UnsafeRawPointer, dataSize: UInt64) {
guard isSettingsStoreAvailable else { return }
let data = Data(bytes: pointer, count: Int(dataSize))
// if writing to the Keychain fails, it will do so silently.
try? SettingsManager.writeAddressCache(data)
}

@_cdecl("swift_read_address_cache")
func readAddressCache() -> SwiftData {
guard isSettingsStoreAvailable else { return SwiftData(data: NSData()) }
let data = (try? SettingsManager.readAddressCache()) ?? Data()
return SwiftData(data: data as NSData)
}
195 changes: 136 additions & 59 deletions ios/MullvadVPN.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion ios/MullvadVPNUITests/BridgingHeader.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@
//

#import <Foundation/Foundation.h>
#include "mullvad-api.h"
215 changes: 107 additions & 108 deletions ios/MullvadVPNUITests/MullvadApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,148 +7,147 @@
//

import Foundation
import MullvadLogging
import MullvadRustRuntime

struct ApiError: Error {
struct MullvadApiError: Error {
let description: String
let kind: MullvadApiErrorKind
init(_ result: MullvadApiError) {
kind = result.kind
if result.description != nil {
description = String(cString: result.description)
} else {
description = "No error"
}
mullvad_api_error_drop(result)
}

func throwIfErr() throws {
if self.kind.rawValue != 0 {
throw self
}
}
}

struct InitMutableBufferError: Error {
let description = "Failed to allocate memory for mutable buffer"
}

struct Device {
let name: String
let id: UUID
}

init(device_struct: MullvadApiDevice) {
name = String(cString: device_struct.name_ptr)
id = UUID(uuid: device_struct.id)
}
private struct NewAccountResponse: Decodable { let number: String }
private struct AccountResponse: Decodable { let expiry: Date }
private struct DeviceResponse: Decodable {
let id: String
let name: String
}

/// - Warning: Do not change the `apiAddress` or the `hostname` after the time `MullvadApi.init` has been invoked
/// The Mullvad API crate is using a global static variable to store those. They will be initialized only once.
///
/// - Warning: Do not change the `apiAddress` or the `hostname` after the time `MullvadApi.init` has been invoked.
class MullvadApi {
private var clientContext = MullvadApiClient()
private let context: SwiftApiContext

private static let logger = Logger(label: "MullvadApi")

/// Initialize the Mullvad API client
/// - Parameters:
/// - apiAddress: Address of the Mullvad API server in the format \<IP-address\>:\<port\>
/// - hostname: Hostname of the Mullvad API server
init(apiAddress: String, hostname: String) throws {
let result = mullvad_api_client_initialize(
&clientContext,
Self.logger.debug("Initializing MullvadApi with address: \(apiAddress), hostname: \(hostname)")
let directRaw = convert_builtin_access_method_setting(
UUID().uuidString, "Direct", true, UInt8(KindDirect.rawValue), nil
)
// Bridges and EncryptedDNS must be disabled because the shadowsocks bridge provider
// is initialized with a nil loader. If Direct fails and the access method selector
// falls back to Bridges, it will dereference the nil pointer and SIGABRT.
let bridgesRaw = convert_builtin_access_method_setting(
UUID().uuidString, "Bridges", false, UInt8(KindBridge.rawValue), nil
)
let encryptedDNSRaw = convert_builtin_access_method_setting(
UUID().uuidString,
"EncryptedDNS",
false,
UInt8(
KindEncryptedDnsProxy.rawValue
),
nil
)
let settingsWrapper = init_access_method_settings_wrapper(
directRaw, bridgesRaw, encryptedDNSRaw, nil, 0
)
let bridgeProvider = SwiftShadowsocksLoaderWrapper(
_0: SwiftShadowsocksLoaderWrapperContext(shadowsocks_loader: nil)
)
context = mullvad_api_init_inner(
hostname,
apiAddress,
hostname,
false
false,
bridgeProvider,
settingsWrapper,
nil,
nil
)
try ApiError(result).throwIfErr()
}

/// Removes all devices assigned to the specified account
func removeAllDevices(forAccount: String) throws {
let result = mullvad_api_remove_all_devices(
clientContext,
forAccount
)
func createAccount() throws -> String {
let response = try makeRequest { cookie, strategy in
mullvad_ios_create_account(context, cookie, strategy)
}
let data = try requireBody(response)
return try JSONDecoder().decode(NewAccountResponse.self, from: data).number
}

try ApiError(result).throwIfErr()
func delete(account: String) throws {
_ = try makeRequest { cookie, strategy in
mullvad_ios_delete_account(context, cookie, strategy, account)
}
}

/// Public key must be at least 32 bytes long - only 32 bytes of it will be read
func addDevice(forAccount: String, publicKey: Data) throws {
var device = MullvadApiDevice()
let result = mullvad_api_add_device(
clientContext,
forAccount,
(publicKey as NSData).bytes,
&device
)

try ApiError(result).throwIfErr()
_ = try publicKey.withUnsafeBytes { ptr -> MullvadApiResponse in
try makeRequest { cookie, strategy in
mullvad_ios_create_device(
context,
cookie,
strategy,
forAccount,
ptr.baseAddress!.assumingMemoryBound(to: UInt8.self)
)
}
}
}

/// Returns a unix timestamp of the expiry date for the specified account.
func getExpiry(forAccount: String) throws -> UInt64 {
var expiry = UInt64(0)
let result = mullvad_api_get_expiry(clientContext, forAccount, &expiry)

try ApiError(result).throwIfErr()

return expiry
}

func createAccount() throws -> String {
var newAccountPtr: UnsafePointer<CChar>?
let result = mullvad_api_create_account(
clientContext,
&newAccountPtr
)
try ApiError(result).throwIfErr()

let newAccount = String(cString: newAccountPtr!)
return newAccount
let response = try makeRequest { cookie, strategy in
mullvad_ios_get_account(context, cookie, strategy, forAccount)
}
let data = try requireBody(response)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try decoder.decode(AccountResponse.self, from: data)
return UInt64(decoded.expiry.timeIntervalSince1970)
}

func listDevices(forAccount: String) throws -> [Device] {
var iterator = MullvadApiDeviceIterator()
let result = mullvad_api_list_devices(clientContext, forAccount, &iterator)
try ApiError(result).throwIfErr()

return DeviceIterator(iter: iterator).collect()
}

func delete(account: String) throws {
let result = mullvad_api_delete_account(clientContext, account)
try ApiError(result).throwIfErr()
let response = try makeRequest { cookie, strategy in
mullvad_ios_get_devices(context, cookie, strategy, forAccount)
}
let data = try requireBody(response)
let deviceResponses = try JSONDecoder().decode([DeviceResponse].self, from: data)
return deviceResponses.compactMap { d in
guard let uuid = UUID(uuidString: d.id) else { return nil }
return Device(name: d.name, id: uuid)
}
}

deinit {
mullvad_api_client_drop(clientContext)
private func requireBody(_ response: MullvadApiResponse) throws -> Data {
guard response.success, let data = response.body else {
throw MullvadApiError(description: response.errorDescription ?? "Request failed")
}
return data
}

class DeviceIterator {
private let backingIter: MullvadApiDeviceIterator

init(iter: MullvadApiDeviceIterator) {
backingIter = iter
}
@discardableResult
private func makeRequest(
_ call: (UnsafeMutableRawPointer, SwiftRetryStrategy) -> SwiftCancelHandle
) throws -> MullvadApiResponse {
let semaphore = DispatchSemaphore(value: 0)
nonisolated(unsafe) var apiResponse: MullvadApiResponse?

func collect() -> [Device] {
var nextDevice = MullvadApiDevice()
var devices: [Device] = []
while mullvad_api_device_iter_next(backingIter, &nextDevice) {
devices.append(Device(device_struct: nextDevice))
mullvad_api_device_drop(nextDevice)
}
return devices
let completion = MullvadApiCompletion { response in
apiResponse = response
semaphore.signal()
}

deinit {
mullvad_api_device_iter_drop(backingIter)
let cookie = Unmanaged.passRetained(completion).toOpaque()
let strategy = mullvad_api_retry_strategy_constant(3, 1)
var handle = call(cookie, strategy)
semaphore.wait()
mullvad_api_cancel_task_drop(&handle)

guard let response = apiResponse else {
throw MullvadApiError(description: "No response received")
}
}
}

private extension String {
func lengthOfBytes() -> UInt {
return UInt(self.lengthOfBytes(using: String.Encoding.utf8))
return response
}
}
2 changes: 2 additions & 0 deletions ios/MullvadVPNUITests/Networking/MullvadAPIWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import CryptoKit
import Foundation
import MullvadRustRuntime
import XCTest

enum MullvadAPIError: Error {
Expand All @@ -32,6 +33,7 @@ class MullvadAPIWrapper: @unchecked Sendable {
.infoDictionary?["ApiEndpoint"] as! String

init() throws {
RustLogging.initialize()
let apiAddress = try Self.getAPIIPAddress() + ":" + Self.getAPIPort()
let hostname = Self.hostName
mullvadAPI = try MullvadApi(apiAddress: apiAddress, hostname: hostname)
Expand Down
5 changes: 0 additions & 5 deletions mullvad-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,13 @@ tokio-rustls = { version = "0.26.0", default-features = false, features = [
tokio-socks = "0.5.1"
tower = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }
uuid = { version = "1.4.1", features = ["v4"] }
vec1 = { workspace = true, features = ["serde"] }
webpki-roots = { workspace = true, optional = true }

[dev-dependencies]
mockito = "1.6.1"
talpid-time = { path = "../talpid-time", features = ["test"] }
tokio = { workspace = true, features = ["test-util", "time"] }

[build-dependencies]
cbindgen = { version = "0.28.0", default-features = false }

[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies]
mullvad-update = { path = "../mullvad-update", features = ["client"] }

Expand Down
15 changes: 0 additions & 15 deletions mullvad-api/build.rs

This file was deleted.

Loading
Loading