summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-02-14 17:45:46 +0100
committerBug Magnet <marco.nikic@mullvad.net>2024-02-14 17:45:46 +0100
commitfb4b924ffe86f92793e087debbb86a87e91a79fc (patch)
treee15eccd84eaddc05c4963037c234d2668622421c
parente2c4fb47f4352cf1eb1200fbb29b9539c86b73e3 (diff)
parentc77c1a4f7c2ce35344c65a389d5d1ece198eacfa (diff)
downloadmullvadvpn-fb4b924ffe86f92793e087debbb86a87e91a79fc.tar.xz
mullvadvpn-fb4b924ffe86f92793e087debbb86a87e91a79fc.zip
Merge branch 'add-a-way-to-interact-with-our-apis-from-our-unit-tests-ios-475'
-rw-r--r--Cargo.lock2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj75
-rw-r--r--ios/MullvadVPNUITests/BridgingHeader.h10
-rw-r--r--ios/MullvadVPNUITests/MullvadApi.swift150
-rw-r--r--mullvad-api/Cargo.toml10
-rw-r--r--mullvad-api/build.rs12
-rw-r--r--mullvad-api/include/mullvad-api.h165
-rw-r--r--mullvad-api/src/address_cache.rs5
-rw-r--r--mullvad-api/src/ffi/device.rs104
-rw-r--r--mullvad-api/src/ffi/error.rs62
-rw-r--r--mullvad-api/src/ffi/mod.rs428
-rw-r--r--mullvad-api/src/lib.rs60
-rw-r--r--mullvad-api/src/rest.rs15
-rw-r--r--mullvad-daemon/src/device/service.rs4
-rw-r--r--talpid-time/src/unix.rs2
15 files changed, 1091 insertions, 13 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6ebbcde633..09cdeb8401 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1814,6 +1814,7 @@ dependencies = [
name = "mullvad-api"
version = "0.0.0"
dependencies = [
+ "cbindgen",
"chrono",
"err-derive",
"futures",
@@ -1834,6 +1835,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tokio-socks",
+ "uuid",
]
[[package]]
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 8fc4ca5556..0a669b3f87 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ 01EF6F2A2B6A473900125696 /* MullvadApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EF6F292B6A473900125696 /* MullvadApi.swift */; };
+ 01EF6F342B6A590700125696 /* libmullvad_api.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01EF6F332B6A590700125696 /* libmullvad_api.a */; };
062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 06799AB428F98CE700ACD94E /* le_root_cert.cer */; };
062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */; };
063687BA28EB234F00BE7161 /* PacketTunnelTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687B928EB234F00BE7161 /* PacketTunnelTransport.swift */; };
@@ -585,7 +587,6 @@
850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */; };
850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */; };
850201E32B51A93C00EF8C96 /* SettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201E22B51A93C00EF8C96 /* SettingsPage.swift */; };
- 8518F6382B60157E009EB113 /* LoggedInWithoutTimeUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8518F6372B60157E009EB113 /* LoggedInWithoutTimeUITestCase.swift */; };
852969282B4D9C1F007EAD4C /* AccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852969272B4D9C1F007EAD4C /* AccountTests.swift */; };
852969332B4E9232007EAD4C /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852969322B4E9232007EAD4C /* Page.swift */; };
852969352B4E9270007EAD4C /* LoginPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852969342B4E9270007EAD4C /* LoginPage.swift */; };
@@ -761,6 +762,9 @@
A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */; };
A9C342C52ACC42130045F00E /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */; };
A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */; };
+ A9DF789B2B7D1DF10094E4AD /* mullvad-api.h in Headers */ = {isa = PBXBuildFile; fileRef = 01EF6F2D2B6A51B100125696 /* mullvad-api.h */; settings = {ATTRIBUTES = (Private, ); }; };
+ A9DF789C2B7D1E410094E4AD /* BridgingHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 01EF6F352B6A5AEF00125696 /* BridgingHeader.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859089692B61763B003AF5F5 /* LoggedInWithTimeUITestCase.swift */; };
A9E031782ACB09930095D843 /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E031762ACB08950095D843 /* UIApplication+Extensions.swift */; };
A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */; };
A9E0317C2ACBFC7E0095D843 /* TunnelStore+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */; };
@@ -1213,6 +1217,12 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 01EF6F292B6A473900125696 /* MullvadApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApi.swift; sourceTree = "<group>"; };
+ 01EF6F2D2B6A51B100125696 /* mullvad-api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "mullvad-api.h"; path = "../../mullvad-api/include/mullvad-api.h"; sourceTree = "<group>"; };
+ 01EF6F2F2B6A588300125696 /* aarch64-apple-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "aarch64-apple-ios"; path = "../target/aarch64-apple-ios"; sourceTree = "<group>"; };
+ 01EF6F312B6A58F000125696 /* debug */ = {isa = PBXFileReference; lastKnownFileType = folder; name = debug; path = "../target/aarch64-apple-ios/debug"; sourceTree = "<group>"; };
+ 01EF6F332B6A590700125696 /* libmullvad_api.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmullvad_api.a; path = "../target/aarch64-apple-ios/debug/libmullvad_api.a"; sourceTree = "<group>"; };
+ 01EF6F352B6A5AEF00125696 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libshadowsocks_proxy.a; path = ../target/debug/libshadowsocks_proxy.a; sourceTree = "<group>"; };
062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTDefaults.swift; sourceTree = "<group>"; };
063687AF28EB083800BE7161 /* ProxyURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyURLRequest.swift; sourceTree = "<group>"; };
@@ -2093,6 +2103,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 01EF6F342B6A590700125696 /* libmullvad_api.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2597,6 +2608,9 @@
584F991F2902CBDD001F858D /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 01EF6F332B6A590700125696 /* libmullvad_api.a */,
+ 01EF6F312B6A58F000125696 /* debug */,
+ 01EF6F2F2B6A588300125696 /* aarch64-apple-ios */,
584023282A407F5F007B27AC /* libtunnel_obfuscator_proxy.a */,
01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */,
);
@@ -3417,7 +3431,10 @@
children = (
852969272B4D9C1F007EAD4C /* AccountTests.swift */,
85557B112B594FC900795FE1 /* ConnectivityTests.swift */,
+ 01EF6F352B6A5AEF00125696 /* BridgingHeader.h */,
852969372B4ED20E007EAD4C /* Info.plist */,
+ 01EF6F2D2B6A51B100125696 /* mullvad-api.h */,
+ 01EF6F292B6A473900125696 /* MullvadApi.swift */,
85557B0C2B591B0F00795FE1 /* Networking */,
852969312B4E9220007EAD4C /* Pages */,
850201DA2B503D7700EF8C96 /* RelayTests.swift */,
@@ -3649,6 +3666,15 @@
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
+ 01EF6F2C2B6A517900125696 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A9DF789C2B7D1E410094E4AD /* BridgingHeader.h in Headers */,
+ A9DF789B2B7D1DF10094E4AD /* mullvad-api.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
06799AB728F98E1D00ACD94E /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
@@ -4116,6 +4142,8 @@
isa = PBXNativeTarget;
buildConfigurationList = 8529692F2B4D9C1F007EAD4C /* Build configuration list for PBXNativeTarget "MullvadVPNUITests" */;
buildPhases = (
+ 01EF6F2C2B6A517900125696 /* Headers */,
+ 01EF6F2B2B6A512C00125696 /* ShellScript */,
852969212B4D9C1F007EAD4C /* Sources */,
852969222B4D9C1F007EAD4C /* Frameworks */,
852969232B4D9C1F007EAD4C /* Resources */,
@@ -4382,6 +4410,24 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
+ 01EF6F2B2B6A512C00125696 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nCARGO_TARGET_DIR=${PROJECT_DIR}/../target bash ${PROJECT_DIR}/build-rust-library.sh mullvad-api\n";
+ };
580E3F212A9860F20061809D /* Run SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -5332,6 +5378,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */,
85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */,
8529693C2B4F0257007EAD4C /* Alert.swift in Sources */,
850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */,
@@ -5344,11 +5391,11 @@
85557B202B5FBBD700795FE1 /* AccountPage.swift in Sources */,
852969352B4E9270007EAD4C /* LoginPage.swift in Sources */,
850201E32B51A93C00EF8C96 /* SettingsPage.swift in Sources */,
- 8518F6382B60157E009EB113 /* LoggedInWithoutTimeUITestCase.swift in Sources */,
85557B102B59215F00795FE1 /* FirewallRule.swift in Sources */,
850201E32B51A93C00EF8C96 /* SettingsPage.swift in Sources */,
85557B0E2B591B2600795FE1 /* FirewallAPIClient.swift in Sources */,
852969282B4D9C1F007EAD4C /* AccountTests.swift in Sources */,
+ 01EF6F2A2B6A473900125696 /* MullvadApi.swift in Sources */,
85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */,
855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */,
8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */,
@@ -6705,11 +6752,15 @@
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = CKG9MXH72F;
"DEVELOPMENT_TEAM[sdk=macosx*]" = CKG9MXH72F;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
+ HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../mullvad-api/include";
INFOPLIST_FILE = MullvadVPNUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.2;
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/debug";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/debug";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadVPNUITests";
@@ -6719,6 +6770,7 @@
SECURITY_GROUP_IDENTIFIER = group.net.mullvad.MullvadVPN;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = MullvadVPN;
@@ -6730,17 +6782,22 @@
buildSettings = {
APPLICATION_IDENTIFIER = net.mullvad.MullvadVPN;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
+ HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../mullvad-api/include";
INFOPLIST_FILE = MullvadVPNUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.2;
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/release";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/release";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/release";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadVPNUITests";
PRODUCT_NAME = "$(TARGET_NAME)";
SECURITY_GROUP_IDENTIFIER = group.net.mullvad.MullvadVPN;
SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = MullvadVPN;
@@ -7323,14 +7380,24 @@
A93A1D062B59145C00F7796C /* Staging */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../mullvad-api/include";
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/debug";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/debug";
PRODUCT_NAME = MullvadVPNUITests;
+ SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h";
};
name = Staging;
};
A93A1D072B59145C00F7796C /* MockRelease */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../mullvad-api/include";
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/release";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/release";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/release";
PRODUCT_NAME = MullvadVPNUITests;
+ SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h";
};
name = MockRelease;
};
diff --git a/ios/MullvadVPNUITests/BridgingHeader.h b/ios/MullvadVPNUITests/BridgingHeader.h
new file mode 100644
index 0000000000..249e46c9a9
--- /dev/null
+++ b/ios/MullvadVPNUITests/BridgingHeader.h
@@ -0,0 +1,10 @@
+//
+// BridgingHeader.h
+// MullvadVPN
+//
+// Created by Emils on 31/01/2024.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#include "mullvad-api.h"
diff --git a/ios/MullvadVPNUITests/MullvadApi.swift b/ios/MullvadVPNUITests/MullvadApi.swift
new file mode 100644
index 0000000000..28841340f7
--- /dev/null
+++ b/ios/MullvadVPNUITests/MullvadApi.swift
@@ -0,0 +1,150 @@
+//
+// MullvadApi.swift
+// MullvadVPNUITests
+//
+// Created by Emils on 31/01/2024.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+struct ApiError: 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"
+}
+
+class MullvadApi {
+ private var clientContext = MullvadApiClient()
+
+ init(apiAddress: String, hostname: String) throws {
+ let result = mullvad_api_client_initialize(
+ &clientContext,
+ apiAddress,
+ hostname
+ )
+ 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
+ )
+
+ try ApiError(result).throwIfErr()
+ }
+
+ /// 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()
+ }
+
+ /// 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 {
+ guard let data = NSMutableData(length: 128) else {
+ throw InitMutableBufferError()
+ }
+
+ var newAccountPtr: UnsafePointer<CChar>?
+ let result = mullvad_api_create_account(
+ clientContext,
+ &newAccountPtr
+ )
+ try ApiError(result).throwIfErr()
+
+ let newAccount = String(cString: newAccountPtr!)
+ return newAccount
+ }
+
+ 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()
+ }
+
+ deinit {
+ mullvad_api_client_drop(clientContext)
+ }
+
+ 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)
+ }
+ }
+
+ class DeviceIterator {
+ private let backingIter: MullvadApiDeviceIterator
+
+ init(iter: MullvadApiDeviceIterator) {
+ backingIter = iter
+ }
+
+ 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
+ }
+
+ deinit {
+ mullvad_api_device_iter_drop(backingIter)
+ }
+ }
+}
+
+private extension String {
+ func lengthOfBytes() -> UInt {
+ return UInt(self.lengthOfBytes(using: String.Encoding.utf8))
+ }
+}
diff --git a/mullvad-api/Cargo.toml b/mullvad-api/Cargo.toml
index 684eacffa9..b81725bbbb 100644
--- a/mullvad-api/Cargo.toml
+++ b/mullvad-api/Cargo.toml
@@ -37,3 +37,13 @@ talpid-types = { path = "../talpid-types" }
talpid-time = { path = "../talpid-time" }
shadowsocks = { workspace = true, features = [ "stream-cipher" ] }
+
+[build-dependencies]
+cbindgen = { version = "0.24.3", default-features = false }
+
+[target.'cfg(target_os = "ios")'.dependencies]
+uuid = { version = "1.4.1", features = ["v4"] }
+
+[lib]
+crate-type = [ "rlib", "staticlib" ]
+bench = false
diff --git a/mullvad-api/build.rs b/mullvad-api/build.rs
new file mode 100644
index 0000000000..cddf6e079d
--- /dev/null
+++ b/mullvad-api/build.rs
@@ -0,0 +1,12 @@
+fn main() {
+ let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
+
+ if std::env::var("TARGET").unwrap() == "aarch64-apple-ios" {
+ cbindgen::Builder::new()
+ .with_crate(crate_dir)
+ .with_language(cbindgen::Language::C)
+ .generate()
+ .expect("failed to generate bindings")
+ .write_to_file("include/mullvad-api.h");
+ }
+}
diff --git a/mullvad-api/include/mullvad-api.h b/mullvad-api/include/mullvad-api.h
new file mode 100644
index 0000000000..2c7600f842
--- /dev/null
+++ b/mullvad-api/include/mullvad-api.h
@@ -0,0 +1,165 @@
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+typedef enum MullvadApiErrorKind {
+ NoError = 0,
+ StringParsing = -1,
+ SocketAddressParsing = -2,
+ AsyncRuntimeInitialization = -3,
+ BadResponse = -4,
+} MullvadApiErrorKind;
+
+typedef struct DeviceIterator DeviceIterator;
+
+/**
+ * A Mullvad API client that can be used via a C FFI.
+ */
+typedef struct FfiClient FfiClient;
+
+/**
+ * MullvadApiErrorKind contains a description and an error kind. If the error kind is
+ * `MullvadApiErrorKind` is NoError, the pointer will be nil.
+ */
+typedef struct MullvadApiError {
+ char *description;
+ enum MullvadApiErrorKind kind;
+} MullvadApiError;
+
+typedef struct MullvadApiClient {
+ const struct FfiClient *ptr;
+} MullvadApiClient;
+
+typedef struct MullvadApiDeviceIterator {
+ struct DeviceIterator *ptr;
+} MullvadApiDeviceIterator;
+
+typedef struct MullvadApiDevice {
+ const char *name_ptr;
+ uint8_t id[16];
+} MullvadApiDevice;
+
+/**
+ * Initializes a Mullvad API client.
+ *
+ * #Arguments
+ * * `client_ptr`: Must be a pointer to that is valid for the length of a `MullvadApiClient`
+ * struct.
+ *
+ * * `api_address`: pointer to nul-terminated UTF-8 string containing a socket address
+ * representation
+ * ("143.32.4.32:9090"), the port is mandatory.
+ *
+ * * `hostname`: pointer to a null-terminated UTF-8 string representing the hostname that will be
+ * used for TLS validation.
+ */
+struct MullvadApiError mullvad_api_client_initialize(struct MullvadApiClient *client_ptr,
+ const char *api_address_ptr,
+ const char *hostname);
+
+/**
+ * Removes all devices from a given account
+ *
+ * #Arguments
+ * * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+ *
+ * * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the
+ * account that will have all of it's devices removed.
+ */
+struct MullvadApiError mullvad_api_remove_all_devices(struct MullvadApiClient client_ptr,
+ const char *account_ptr);
+
+/**
+ * Removes all devices from a given account
+ *
+ * #Arguments
+ * * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+ *
+ * * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the
+ * account that will have all of it's devices removed.
+ *
+ * * `expiry_unix_timestamp`: a pointer to a signed 64 bit integer. If this function returns no
+ * error, the expiry timestamp will be written to this pointer.
+ */
+struct MullvadApiError mullvad_api_get_expiry(struct MullvadApiClient client_ptr,
+ const char *account_str_ptr,
+ int64_t *expiry_unix_timestamp);
+
+/**
+ * Gets a list of all devices associated with the specified account from the API.
+ *
+ * #Arguments
+ * * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+ *
+ * * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the
+ * account that will have all of it's devices removed.
+ *
+ * * `device_iter_ptr`: a pointer to a `device::MullvadApiDeviceIterator`. If this function
+ * doesn't return an error, the pointer will be initialized with a valid instance of
+ * `device::MullvadApiDeviceIterator`, which can be used to iterate through the devices.
+ */
+struct MullvadApiError mullvad_api_list_devices(struct MullvadApiClient client_ptr,
+ const char *account_str_ptr,
+ struct MullvadApiDeviceIterator *device_iter_ptr);
+
+/**
+ * Adds a device to the specified account with the specified public key. Note that the device
+ * name, associated addresess and UUID are not returned.
+ *
+ * #Arguments
+ * * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+ *
+ * * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the
+ * account that will have a device added to ita device added to it.
+ *
+ * * `public_key_ptr`: a pointer to 32 bytes of a WireGuard public key that will be uploaded.
+ *
+ * * `new_device_ptr`: a pointer to enough memory to allocate a `MullvadApiDevice`. If this
+ * function doesn't return an error, it will be initialized.
+ */
+struct MullvadApiError mullvad_api_add_device(struct MullvadApiClient client_ptr,
+ const char *account_str_ptr,
+ const uint8_t *public_key_ptr,
+ struct MullvadApiDevice *new_device_ptr);
+
+/**
+ * Creates a new account.
+ *
+ * #Arguments
+ * * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+ *
+ * * `account_str_ptr`: If a new account is created successfully, a pointer to an allocated C
+ * string containing the new
+ * account number will be written to this pointer. It must be freed via
+ * `mullvad_api_cstring_drop`.
+ */
+struct MullvadApiError mullvad_api_create_account(struct MullvadApiClient client_ptr,
+ const char **account_str_ptr);
+
+/**
+ * Deletes the specified account.
+ *
+ * #Arguments
+ * * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+ *
+ * * `account_str_ptr`: A null-terminated string representing the account to be deleted.
+ */
+struct MullvadApiError mullvad_api_delete_account(struct MullvadApiClient client_ptr,
+ const char *account_str_ptr);
+
+void mullvad_api_client_drop(struct MullvadApiClient client);
+
+/**
+ * Deallocates a CString returned by the Mullvad API client.
+ */
+void mullvad_api_cstring_drop(char *cstr_ptr);
+
+bool mullvad_api_device_iter_next(struct MullvadApiDeviceIterator iter,
+ struct MullvadApiDevice *device_ptr);
+
+void mullvad_api_device_iter_drop(struct MullvadApiDeviceIterator iter);
+
+void mullvad_api_device_drop(struct MullvadApiDevice device);
+
+void mullvad_api_error_drop(struct MullvadApiError error);
diff --git a/mullvad-api/src/address_cache.rs b/mullvad-api/src/address_cache.rs
index ea93d96e26..f164e15f09 100644
--- a/mullvad-api/src/address_cache.rs
+++ b/mullvad-api/src/address_cache.rs
@@ -34,6 +34,11 @@ impl AddressCache {
Self::new_inner(API.address(), write_path)
}
+ pub fn with_static_addr(address: SocketAddr) -> Self {
+ Self::new_inner(address, None)
+ .expect("Failed to construct an address cache from a static address")
+ }
+
/// Initialize cache using `read_path`, and write changes to `write_path`.
pub async fn from_file(read_path: &Path, write_path: Option<Box<Path>>) -> Result<Self, Error> {
log::debug!("Loading API addresses from {}", read_path.display());
diff --git a/mullvad-api/src/ffi/device.rs b/mullvad-api/src/ffi/device.rs
new file mode 100644
index 0000000000..82ba5f59cc
--- /dev/null
+++ b/mullvad-api/src/ffi/device.rs
@@ -0,0 +1,104 @@
+use mullvad_types::device::Device;
+use std::{ffi::CString, ptr};
+
+#[repr(C)]
+pub struct MullvadApiDeviceIterator {
+ ptr: *mut DeviceIterator,
+}
+
+impl MullvadApiDeviceIterator {
+ pub fn new(iter: impl IntoIterator<Item = Device> + 'static) -> Self {
+ let iter = Box::new(DeviceIterator::from(iter));
+
+ Self {
+ ptr: Box::into_raw(iter),
+ }
+ }
+
+ fn is_done(&self) -> bool {
+ self.ptr.is_null()
+ }
+
+ unsafe fn as_iter(&mut self) -> &mut Box<dyn Iterator<Item = Device>> {
+ let wrapper = unsafe { &mut *self.ptr };
+ &mut wrapper.iter
+ }
+
+ fn drop(mut self) {
+ if self.ptr.is_null() {
+ return;
+ }
+
+ let _ = unsafe { Box::from_raw(self.ptr) };
+ self.ptr = ptr::null_mut();
+ }
+}
+
+#[repr(C)]
+pub struct MullvadApiDevice {
+ name_ptr: *const libc::c_char,
+ id: [u8; 16],
+}
+
+impl From<Device> for MullvadApiDevice {
+ fn from(dev: Device) -> Self {
+ let name = CString::new(dev.name).expect("Null bytes in name from API response");
+ let name_ptr = name.into_raw();
+ let id = *uuid::Uuid::parse_str(&dev.id)
+ .expect("Failed to parse UUID")
+ .as_bytes();
+
+ Self { name_ptr, id }
+ }
+}
+
+impl MullvadApiDevice {
+ fn drop(self) {
+ let _ = unsafe { CString::from_raw(self.name_ptr as *mut _) };
+ }
+}
+
+struct DeviceIterator {
+ iter: Box<dyn Iterator<Item = Device>>,
+}
+
+impl<T> From<T> for DeviceIterator
+where
+ T: IntoIterator<Item = Device> + 'static,
+{
+ fn from(i: T) -> Self {
+ let iter: Box<dyn Iterator<Item = Device>> = Box::new(i.into_iter());
+ Self { iter }
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn mullvad_api_device_iter_next(
+ mut iter: MullvadApiDeviceIterator,
+ device_ptr: *mut MullvadApiDevice,
+) -> bool {
+ if iter.is_done() {
+ return false;
+ }
+
+ // SAFETY: Asuming self.ptr is still valid since iter.is_done() returned false;
+ let iter = unsafe { iter.as_iter() };
+ let Some(device) = iter.next() else {
+ return false;
+ };
+
+ // SAFETY: Assuming device pointer is valid
+ unsafe { ptr::write(device_ptr, device.into()) }
+
+ return true;
+}
+
+#[no_mangle]
+pub extern "C" fn mullvad_api_device_iter_drop(iter: MullvadApiDeviceIterator) {
+ iter.drop()
+}
+
+#[no_mangle]
+pub extern "C" fn mullvad_api_device_drop(device: MullvadApiDevice) {
+ device.drop()
+}
diff --git a/mullvad-api/src/ffi/error.rs b/mullvad-api/src/ffi/error.rs
new file mode 100644
index 0000000000..8f3095fee0
--- /dev/null
+++ b/mullvad-api/src/ffi/error.rs
@@ -0,0 +1,62 @@
+use crate::rest;
+use std::ffi::CString;
+
+#[derive(Debug, PartialEq)]
+#[repr(C)]
+pub enum MullvadApiErrorKind {
+ NoError = 0,
+ StringParsing = -1,
+ SocketAddressParsing = -2,
+ AsyncRuntimeInitialization = -3,
+ BadResponse = -4,
+}
+
+/// MullvadApiErrorKind contains a description and an error kind. If the error kind is
+/// `MullvadApiErrorKind` is NoError, the pointer will be nil.
+#[repr(C)]
+pub struct MullvadApiError {
+ description: *mut libc::c_char,
+ kind: MullvadApiErrorKind,
+}
+
+impl MullvadApiError {
+ pub fn new(kind: MullvadApiErrorKind, error: &dyn std::error::Error) -> Self {
+ let description = CString::new(format!("{error:?}")).unwrap_or_default();
+ Self {
+ description: description.into_raw(),
+ kind,
+ }
+ }
+
+ pub fn api_err(error: rest::Error) -> Self {
+ Self::new(MullvadApiErrorKind::BadResponse, &error)
+ }
+
+ pub fn with_str(kind: MullvadApiErrorKind, description: &str) -> Self {
+ let description = CString::new(description).unwrap_or_default();
+ Self {
+ description: description.into_raw(),
+ kind,
+ }
+ }
+
+ pub fn ok() -> MullvadApiError {
+ Self {
+ description: std::ptr::null_mut(),
+ kind: MullvadApiErrorKind::NoError,
+ }
+ }
+
+ pub fn drop(self) {
+ if self.description.is_null() {
+ return;
+ }
+
+ let _ = unsafe { CString::from_raw(self.description) };
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn mullvad_api_error_drop(error: MullvadApiError) {
+ error.drop()
+}
diff --git a/mullvad-api/src/ffi/mod.rs b/mullvad-api/src/ffi/mod.rs
new file mode 100644
index 0000000000..ba839f36a5
--- /dev/null
+++ b/mullvad-api/src/ffi/mod.rs
@@ -0,0 +1,428 @@
+use std::{
+ ffi::{CStr, CString},
+ net::SocketAddr,
+ ptr,
+ sync::Arc,
+};
+
+use crate::{
+ rest::{self, MullvadRestHandle},
+ AccountsProxy, DevicesProxy,
+};
+
+mod device;
+mod error;
+
+pub use error::{MullvadApiError, MullvadApiErrorKind};
+
+#[repr(C)]
+pub struct MullvadApiClient {
+ ptr: *const FfiClient,
+}
+
+impl MullvadApiClient {
+ fn new(client: FfiClient) -> Self {
+ let arc = Arc::new(client);
+ let ptr = Arc::into_raw(arc);
+ Self { ptr }
+ }
+
+ unsafe fn get_client(&self) -> Arc<FfiClient> {
+ // Incrementing before creating an Arc from a pointer. This way multiple threads can use
+ // it, and a single thread can decrement it.
+ unsafe { Arc::increment_strong_count(self.ptr) };
+
+ unsafe { Arc::from_raw(self.ptr) }
+ }
+
+ fn drop(self) {
+ if self.ptr.is_null() {
+ return;
+ }
+
+ let _ = unsafe { Arc::from_raw(self.ptr) };
+ }
+}
+
+/// A Mullvad API client that can be used via a C FFI.
+struct FfiClient {
+ tokio_runtime: tokio::runtime::Runtime,
+ api_runtime: crate::Runtime,
+ api_hostname: String,
+}
+
+impl FfiClient {
+ unsafe fn new(
+ api_address_ptr: *const libc::c_char,
+ hostname: *const libc::c_char,
+ ) -> Result<Self, MullvadApiError> {
+ // SAFETY: addr_str must be a valid pointer to a null-terminated string.
+ let addr_str = unsafe { string_from_raw_ptr(api_address_ptr)? };
+ // SAFETY: api_hostname must be a valid pointer to a null-terminated string.
+ let api_hostname = unsafe { string_from_raw_ptr(hostname)? };
+
+ let api_address: SocketAddr = addr_str.parse().map_err(|_| {
+ MullvadApiError::with_str(
+ MullvadApiErrorKind::SocketAddressParsing,
+ "Failed to parse API socket address",
+ )
+ })?;
+
+ let mut runtime_builder = tokio::runtime::Builder::new_multi_thread();
+
+ runtime_builder.worker_threads(2).enable_all();
+ let tokio_runtime = runtime_builder.build().map_err(|err| {
+ MullvadApiError::new(MullvadApiErrorKind::AsyncRuntimeInitialization, &err)
+ })?;
+
+ // It is imperative that the REST runtime is created within an async context, otherwise
+ // ApiAvailability panics.
+ let api_runtime = tokio_runtime.block_on(async {
+ crate::Runtime::with_static_addr(tokio_runtime.handle().clone(), api_address)
+ });
+
+ let context = FfiClient {
+ tokio_runtime,
+ api_runtime,
+ api_hostname,
+ };
+
+ Ok(context)
+ }
+
+ unsafe fn add_device(
+ self: Arc<Self>,
+ account_str_ptr: *const libc::c_char,
+ public_key_ptr: *const u8,
+ ) -> Result<device::MullvadApiDevice, MullvadApiError> {
+ // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string.
+ let account = unsafe { string_from_raw_ptr(account_str_ptr)? };
+
+ // SAFETY: assuming public_key_ptr is valid for 32 bytes
+ let public_key_bytes: [u8; 32] = unsafe { std::ptr::read(public_key_ptr as *const _) };
+ let public_key = public_key_bytes.into();
+
+ let runtime = self.tokio_handle();
+
+ let device_proxy = self.device_proxy();
+
+ let device = runtime
+ .block_on(async move {
+ let (device, _) = device_proxy.create(account, public_key).await?;
+ Ok(device)
+ })
+ .map_err(MullvadApiError::api_err)?;
+
+ Ok(device.into())
+ }
+
+ unsafe fn create_account(self: Arc<Self>) -> Result<String, MullvadApiError> {
+ let accounts_proxy = self.accounts_proxy();
+
+ self.tokio_handle()
+ .block_on(async move {
+ let new_account = accounts_proxy.create_account().await?;
+ Ok(new_account)
+ })
+ .map_err(MullvadApiError::api_err)
+ }
+
+ unsafe fn get_expiry(
+ self: Arc<Self>,
+ account_str_ptr: *const libc::c_char,
+ ) -> Result<i64, MullvadApiError> {
+ // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string.
+ let account = unsafe { string_from_raw_ptr(account_str_ptr)? };
+
+ let account_proxy = self.accounts_proxy();
+ self.tokio_handle()
+ .block_on(async move {
+ let expiry_timestamp = account_proxy.get_data(account).await?.expiry.timestamp();
+ Ok(expiry_timestamp)
+ })
+ .map_err(MullvadApiError::api_err)
+ }
+
+ unsafe fn remove_all_devices(
+ self: Arc<Self>,
+ account_str_ptr: *const libc::c_char,
+ ) -> Result<(), MullvadApiError> {
+ // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string.
+ let account = unsafe { string_from_raw_ptr(account_str_ptr)? };
+
+ let runtime = self.tokio_handle();
+ let device_proxy = self.device_proxy();
+ runtime
+ .block_on(async move {
+ let devices = device_proxy.list(account.clone()).await?;
+ for device in devices {
+ device_proxy.remove(account.clone(), device.id).await?;
+ }
+ Result::<_, rest::Error>::Ok(())
+ })
+ .map_err(MullvadApiError::api_err)
+ }
+
+ unsafe fn list_devices(
+ self: Arc<Self>,
+ account_str_ptr: *const libc::c_char,
+ ) -> Result<device::MullvadApiDeviceIterator, MullvadApiError> {
+ // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string.
+ let account = unsafe { string_from_raw_ptr(account_str_ptr)? };
+
+ let runtime = self.tokio_handle();
+ let device_proxy = self.device_proxy();
+
+ let devices = runtime
+ .block_on(device_proxy.list(account))
+ .map_err(MullvadApiError::api_err)?;
+
+ Ok(device::MullvadApiDeviceIterator::new(devices))
+ }
+
+ unsafe fn delete_account(
+ self: Arc<Self>,
+ account_str_ptr: *const libc::c_char,
+ ) -> Result<(), MullvadApiError> {
+ // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string.
+ let account = unsafe { string_from_raw_ptr(account_str_ptr)? };
+
+ let runtime = self.tokio_handle();
+ let accounts_proxy = self.accounts_proxy();
+
+ runtime
+ .block_on(accounts_proxy.delete_account(account))
+ .map_err(MullvadApiError::api_err)
+ }
+
+ fn rest_handle(&self) -> MullvadRestHandle {
+ self.tokio_runtime.block_on(
+ self.api_runtime
+ .static_mullvad_rest_handle(self.api_hostname.clone()),
+ )
+ }
+
+ fn device_proxy(&self) -> DevicesProxy {
+ crate::DevicesProxy::new(self.rest_handle())
+ }
+
+ fn accounts_proxy(&self) -> AccountsProxy {
+ crate::AccountsProxy::new(self.rest_handle())
+ }
+
+ fn tokio_handle(&self) -> tokio::runtime::Handle {
+ self.tokio_runtime.handle().clone()
+ }
+}
+
+/// Initializes a Mullvad API client.
+///
+/// #Arguments
+/// * `client_ptr`: Must be a pointer to that is valid for the length of a `MullvadApiClient`
+/// struct.
+///
+/// * `api_address`: pointer to nul-terminated UTF-8 string containing a socket address
+/// representation
+/// ("143.32.4.32:9090"), the port is mandatory.
+///
+/// * `hostname`: pointer to a null-terminated UTF-8 string representing the hostname that will be
+/// used for TLS validation.
+#[no_mangle]
+pub extern "C" fn mullvad_api_client_initialize(
+ client_ptr: *mut MullvadApiClient,
+ api_address_ptr: *const libc::c_char,
+ hostname: *const libc::c_char,
+) -> MullvadApiError {
+ match unsafe { FfiClient::new(api_address_ptr, hostname) } {
+ Ok(client) => {
+ unsafe {
+ std::ptr::write(client_ptr, MullvadApiClient::new(client));
+ };
+ MullvadApiError::ok()
+ }
+ Err(err) => err,
+ }
+}
+
+/// Removes all devices from a given account
+///
+/// #Arguments
+/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+///
+/// * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the
+/// account that will have all of it's devices removed.
+#[no_mangle]
+pub extern "C" fn mullvad_api_remove_all_devices(
+ client_ptr: MullvadApiClient,
+ account_ptr: *const libc::c_char,
+) -> MullvadApiError {
+ let client = unsafe { client_ptr.get_client() };
+ match unsafe { client.remove_all_devices(account_ptr) } {
+ Ok(_) => MullvadApiError::ok(),
+ Err(err) => err,
+ }
+}
+
+/// Removes all devices from a given account
+///
+/// #Arguments
+/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+///
+/// * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the
+/// account that will have all of it's devices removed.
+///
+/// * `expiry_unix_timestamp`: a pointer to a signed 64 bit integer. If this function returns no
+/// error, the expiry timestamp will be written to this pointer.
+#[no_mangle]
+pub extern "C" fn mullvad_api_get_expiry(
+ client_ptr: MullvadApiClient,
+ account_str_ptr: *const libc::c_char,
+ expiry_unix_timestamp: *mut i64,
+) -> MullvadApiError {
+ let client = unsafe { client_ptr.get_client() };
+ match unsafe { client.get_expiry(account_str_ptr) } {
+ Ok(expiry) => {
+ unsafe { ptr::write(expiry_unix_timestamp, expiry) };
+ MullvadApiError::ok()
+ }
+ Err(err) => err,
+ }
+}
+
+/// Gets a list of all devices associated with the specified account from the API.
+///
+/// #Arguments
+/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+///
+/// * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the
+/// account that will have all of it's devices removed.
+///
+/// * `device_iter_ptr`: a pointer to a `device::MullvadApiDeviceIterator`. If this function
+/// doesn't return an error, the pointer will be initialized with a valid instance of
+/// `device::MullvadApiDeviceIterator`, which can be used to iterate through the devices.
+#[no_mangle]
+pub extern "C" fn mullvad_api_list_devices(
+ client_ptr: MullvadApiClient,
+ account_str_ptr: *const libc::c_char,
+ device_iter_ptr: *mut device::MullvadApiDeviceIterator,
+) -> MullvadApiError {
+ let client = unsafe { client_ptr.get_client() };
+ match unsafe { client.list_devices(account_str_ptr) } {
+ Ok(iter) => {
+ unsafe { ptr::write(device_iter_ptr, iter) };
+ MullvadApiError::ok()
+ }
+ Err(err) => err,
+ }
+}
+
+/// Adds a device to the specified account with the specified public key. Note that the device
+/// name, associated addresess and UUID are not returned.
+///
+/// #Arguments
+/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+///
+/// * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the
+/// account that will have a device added to ita device added to it.
+///
+/// * `public_key_ptr`: a pointer to 32 bytes of a WireGuard public key that will be uploaded.
+///
+/// * `new_device_ptr`: a pointer to enough memory to allocate a `MullvadApiDevice`. If this
+/// function doesn't return an error, it will be initialized.
+#[no_mangle]
+pub extern "C" fn mullvad_api_add_device(
+ client_ptr: MullvadApiClient,
+ account_str_ptr: *const libc::c_char,
+ public_key_ptr: *const u8,
+ new_device_ptr: *mut device::MullvadApiDevice,
+) -> MullvadApiError {
+ // SAFETY: Assuming MullvadApiClient is initialized
+ let client = unsafe { client_ptr.get_client() };
+ // SAFETY: Asuming `new_device_ptr` is valid.
+ match unsafe { client.add_device(account_str_ptr, public_key_ptr) } {
+ Ok(device) => {
+ // SAFETY: Asuming `new_device_ptr` is valid.
+ // SAFETY: Asuming `new_device_ptr` is valid.
+ unsafe { ptr::write(new_device_ptr, device) };
+ MullvadApiError::ok()
+ }
+ Err(err) => err,
+ }
+}
+
+/// Creates a new account.
+///
+/// #Arguments
+/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+///
+/// * `account_str_ptr`: If a new account is created successfully, a pointer to an allocated C
+/// string containing the new
+/// account number will be written to this pointer. It must be freed via
+/// `mullvad_api_cstring_drop`.
+#[no_mangle]
+pub extern "C" fn mullvad_api_create_account(
+ client_ptr: MullvadApiClient,
+ account_str_ptr: *mut *const libc::c_char,
+) -> MullvadApiError {
+ let client = unsafe { client_ptr.get_client() };
+ match unsafe { client.create_account() } {
+ Ok(new_account) => {
+ let Ok(account) = CString::new(new_account) else {
+ return MullvadApiError::with_str(
+ MullvadApiErrorKind::BadResponse,
+ "Account number string c ontained null bytes",
+ );
+ };
+
+ unsafe { ptr::write(account_str_ptr, account.into_raw()) };
+ MullvadApiError::ok()
+ }
+ Err(err) => err,
+ }
+}
+
+/// Deletes the specified account.
+///
+/// #Arguments
+/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient`
+///
+/// * `account_str_ptr`: A null-terminated string representing the account to be deleted.
+#[no_mangle]
+pub extern "C" fn mullvad_api_delete_account(
+ client_ptr: MullvadApiClient,
+ account_str_ptr: *const libc::c_char,
+) -> MullvadApiError {
+ let client = unsafe { client_ptr.get_client() };
+ match unsafe { client.delete_account(account_str_ptr) } {
+ Ok(_) => MullvadApiError::ok(),
+ Err(err) => err,
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn mullvad_api_client_drop(client: MullvadApiClient) {
+ client.drop()
+}
+
+/// Deallocates a CString returned by the Mullvad API client.
+#[no_mangle]
+pub extern "C" fn mullvad_api_cstring_drop(cstr_ptr: *mut libc::c_char) {
+ let _ = unsafe { CString::from_raw(cstr_ptr) };
+}
+
+/// The return value is only valid for the lifetime of the `ptr` that's passed in
+///
+/// SAFETY: `ptr` must be valid for `size` bytes
+unsafe fn string_from_raw_ptr(ptr: *const libc::c_char) -> Result<String, MullvadApiError> {
+ let cstr = unsafe { CStr::from_ptr(ptr) };
+
+ Ok(cstr
+ .to_str()
+ .map_err(|_| {
+ MullvadApiError::with_str(
+ MullvadApiErrorKind::StringParsing,
+ "Failed to parse UTF-8 string",
+ )
+ })?
+ .to_owned())
+}
diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs
index 40ab7395cd..80031bc13a 100644
--- a/mullvad-api/src/lib.rs
+++ b/mullvad-api/src/lib.rs
@@ -35,6 +35,10 @@ mod access;
mod address_cache;
pub mod device;
mod relay_list;
+
+#[cfg(target_os = "ios")]
+pub mod ffi;
+
pub use address_cache::AddressCache;
pub use device::DevicesProxy;
pub use hyper::StatusCode;
@@ -307,6 +311,17 @@ impl Runtime {
)
}
+ // TODO: gate for ios only
+ pub fn with_static_addr(handle: tokio::runtime::Handle, address: SocketAddr) -> Self {
+ Runtime {
+ handle,
+ address_cache: AddressCache::with_static_addr(address),
+ api_availability: ApiAvailability::new(availability::State::default()),
+ #[cfg(target_os = "android")]
+ socket_bypass_tx,
+ }
+ }
+
fn new_inner(
handle: tokio::runtime::Handle,
#[cfg(target_os = "android")] socket_bypass_tx: Option<mpsc::Sender<SocketBypassRequest>>,
@@ -412,6 +427,27 @@ impl Runtime {
)
}
+ /// This is only to be used in test code
+ pub async fn static_mullvad_rest_handle(&self, hostname: String) -> rest::MullvadRestHandle {
+ let service = self
+ .new_request_service(
+ Some(hostname.clone()),
+ futures::stream::repeat(ApiConnectionMode::Direct),
+ #[cfg(target_os = "android")]
+ self.socket_bypass_tx.clone(),
+ )
+ .await;
+ let token_store = access::AccessTokenStore::new(service.clone());
+ let factory = rest::RequestFactory::new(hostname, Some(token_store));
+
+ rest::MullvadRestHandle::new(
+ service,
+ factory,
+ self.address_cache.clone(),
+ self.availability_handle(),
+ )
+ }
+
/// Returns a new request service handle
pub async fn rest_handle(&self) -> rest::RequestServiceHandle {
self.new_request_service(
@@ -458,7 +494,7 @@ impl AccountsProxy {
}
}
- pub fn create_account(&mut self) -> impl Future<Output = Result<AccountToken, rest::Error>> {
+ pub fn create_account(&self) -> impl Future<Output = Result<AccountToken, rest::Error>> {
#[derive(serde::Deserialize)]
struct AccountCreationResponse {
number: AccountToken,
@@ -478,7 +514,7 @@ impl AccountsProxy {
}
pub fn submit_voucher(
- &mut self,
+ &self,
account: AccountToken,
voucher_code: String,
) -> impl Future<Output = Result<VoucherSubmission, rest::Error>> {
@@ -500,6 +536,26 @@ impl AccountsProxy {
}
}
+ #[cfg(target_os = "ios")]
+ pub fn delete_account(
+ &self,
+ account: AccountToken,
+ ) -> impl Future<Output = Result<(), rest::Error>> {
+ let service = self.handle.service.clone();
+ let factory = self.handle.factory.clone();
+
+ async move {
+ let request = factory
+ .delete(&format!("{ACCOUNTS_URL_PREFIX}/accounts/me"))?
+ .account(account.clone())?
+ .header("Mullvad-Account-Number", &account)?
+ .expected_status(&[StatusCode::NO_CONTENT]);
+
+ let _ = service.request(request).await?;
+ Ok(())
+ }
+ }
+
#[cfg(target_os = "android")]
pub fn init_play_purchase(
&mut self,
diff --git a/mullvad-api/src/rest.rs b/mullvad-api/src/rest.rs
index f0838f918d..0560642bb0 100644
--- a/mullvad-api/src/rest.rs
+++ b/mullvad-api/src/rest.rs
@@ -19,6 +19,7 @@ use hyper::{
};
use mullvad_types::account::AccountToken;
use std::{
+ borrow::Cow,
error::Error as StdError,
str::FromStr,
sync::{Arc, Weak},
@@ -440,15 +441,18 @@ struct NewErrorResponse {
#[derive(Clone)]
pub struct RequestFactory {
- hostname: &'static str,
+ hostname: Cow<'static, str>,
token_store: Option<AccessTokenStore>,
default_timeout: Duration,
}
impl RequestFactory {
- pub fn new(hostname: &'static str, token_store: Option<AccessTokenStore>) -> Self {
+ pub fn new(
+ hostname: impl Into<Cow<'static, str>>,
+ token_store: Option<AccessTokenStore>,
+ ) -> Self {
Self {
- hostname,
+ hostname: hostname.into(),
token_store,
default_timeout: DEFAULT_TIMEOUT,
}
@@ -523,7 +527,10 @@ impl RequestFactory {
.uri(uri)
.header(header::USER_AGENT, HeaderValue::from_static(USER_AGENT))
.header(header::ACCEPT, HeaderValue::from_static("application/json"))
- .header(header::HOST, HeaderValue::from_static(self.hostname));
+ .header(
+ header::HOST,
+ HeaderValue::from_str(&self.hostname).map_err(|_| Error::InvalidHeaderError)?,
+ );
let result = request.body(hyper::Body::empty())?;
Ok(result)
diff --git a/mullvad-daemon/src/device/service.rs b/mullvad-daemon/src/device/service.rs
index 580d993edc..4f62466b6f 100644
--- a/mullvad-daemon/src/device/service.rs
+++ b/mullvad-daemon/src/device/service.rs
@@ -262,7 +262,7 @@ pub struct AccountService {
impl AccountService {
pub fn create_account(&self) -> impl Future<Output = Result<AccountToken, rest::Error>> {
- let mut proxy = self.proxy.clone();
+ let proxy = self.proxy.clone();
let api_handle = self.api_availability.clone();
retry_future(
move || proxy.create_account(),
@@ -308,7 +308,7 @@ impl AccountService {
account_token: AccountToken,
voucher: String,
) -> Result<VoucherSubmission, Error> {
- let mut proxy = self.proxy.clone();
+ let proxy = self.proxy.clone();
let api_handle = self.api_availability.clone();
let result = retry_future(
move || proxy.submit_voucher(account_token.clone(), voucher.clone()),
diff --git a/talpid-time/src/unix.rs b/talpid-time/src/unix.rs
index 643a7049aa..5b247b2f5e 100644
--- a/talpid-time/src/unix.rs
+++ b/talpid-time/src/unix.rs
@@ -3,7 +3,7 @@ use std::{mem::MaybeUninit, time::Duration};
const NSEC_PER_SEC: c_long = 1_000_000_000;
-#[cfg(target_os = "macos")]
+#[cfg(any(target_os = "macos", target_os = "ios"))]
const CLOCK_ID: clockid_t = libc::CLOCK_MONOTONIC;
#[cfg(any(target_os = "linux", target_os = "android"))]