diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2024-02-14 17:45:46 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2024-02-14 17:45:46 +0100 |
| commit | fb4b924ffe86f92793e087debbb86a87e91a79fc (patch) | |
| tree | e15eccd84eaddc05c4963037c234d2668622421c | |
| parent | e2c4fb47f4352cf1eb1200fbb29b9539c86b73e3 (diff) | |
| parent | c77c1a4f7c2ce35344c65a389d5d1ece198eacfa (diff) | |
| download | mullvadvpn-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.lock | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 75 | ||||
| -rw-r--r-- | ios/MullvadVPNUITests/BridgingHeader.h | 10 | ||||
| -rw-r--r-- | ios/MullvadVPNUITests/MullvadApi.swift | 150 | ||||
| -rw-r--r-- | mullvad-api/Cargo.toml | 10 | ||||
| -rw-r--r-- | mullvad-api/build.rs | 12 | ||||
| -rw-r--r-- | mullvad-api/include/mullvad-api.h | 165 | ||||
| -rw-r--r-- | mullvad-api/src/address_cache.rs | 5 | ||||
| -rw-r--r-- | mullvad-api/src/ffi/device.rs | 104 | ||||
| -rw-r--r-- | mullvad-api/src/ffi/error.rs | 62 | ||||
| -rw-r--r-- | mullvad-api/src/ffi/mod.rs | 428 | ||||
| -rw-r--r-- | mullvad-api/src/lib.rs | 60 | ||||
| -rw-r--r-- | mullvad-api/src/rest.rs | 15 | ||||
| -rw-r--r-- | mullvad-daemon/src/device/service.rs | 4 | ||||
| -rw-r--r-- | talpid-time/src/unix.rs | 2 |
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"))] |
