diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-04-15 15:53:59 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-04-15 15:53:59 +0200 |
| commit | c90a98e3430c8047b17b12ca9646af6d4478d3ea (patch) | |
| tree | d0f5fa50299da5bfeb65ed591e6c16c70e5cc4dc /ios | |
| parent | effafb699b8f6831cbc6df0bcaa51766d9a44718 (diff) | |
| parent | 14db956bcd66b841734ed86b647e95f69034bffe (diff) | |
| download | mullvadvpn-c90a98e3430c8047b17b12ca9646af6d4478d3ea.tar.xz mullvadvpn-c90a98e3430c8047b17b12ca9646af6d4478d3ea.zip | |
Merge branch 'implement-sendproblemreport-using-mullvad-api-ios-1134-2'
Diffstat (limited to 'ios')
12 files changed, 265 insertions, 38 deletions
diff --git a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift index 1330be345f..9880512524 100644 --- a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift @@ -37,7 +37,7 @@ struct APIProxyStub: APIQuerying { } func sendProblemReport( - _ body: REST.ProblemReportRequest, + _ body: ProblemReportRequest, retryStrategy: REST.RetryStrategy, completionHandler: @escaping ProxyCompletionHandler<Void> ) -> Cancellable { diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift index 5d2555f07f..5cf3ee0e37 100644 --- a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift +++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift @@ -29,7 +29,7 @@ public protocol APIQuerying: Sendable { ) -> any RESTRequestExecutor<REST.CreateApplePaymentResponse> func sendProblemReport( - _ body: REST.ProblemReportRequest, + _ body: ProblemReportRequest, retryStrategy: REST.RetryStrategy, completionHandler: @escaping @Sendable ProxyCompletionHandler<Void> ) -> Cancellable @@ -122,7 +122,11 @@ extension REST { retryStrategy: REST.RetryStrategy, completionHandler: @escaping ProxyCompletionHandler<Void> ) -> Cancellable { - AnyCancellable() + createNetworkOperation( + request: .sendProblemReport(retryStrategy, problemReportRequest: body), + responseHandler: rustEmptyResponseHandler(), + completionHandler: completionHandler + ) } public func submitVoucher( @@ -134,7 +138,7 @@ extension REST { AnyCancellable() } - private func createNetworkOperation<Success: Decodable>( + private func createNetworkOperation<Success: Any>( request: APIRequest, responseHandler: RustResponseHandler<Success>, completionHandler: @escaping @Sendable ProxyCompletionHandler<Success> @@ -200,20 +204,6 @@ extension REST { let timeAdded: Int let newExpiry: Date } - - public struct ProblemReportRequest: Encodable, Sendable { - public let address: String - public let message: String - public let log: String - public let metadata: [String: String] - - public init(address: String, message: String, log: String, metadata: [String: String]) { - self.address = address - self.message = message - self.log = log - self.metadata = metadata - } - } } // TODO: Remove when "createApplePayment" func is implemented. diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift index 68e7e41663..92b6bb89b1 100644 --- a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift @@ -5,10 +5,13 @@ // Created by Jon Petersson on 2025-02-24. // Copyright © 2025 Mullvad VPN AB. All rights reserved. // +import MullvadTypes public enum APIRequest: Codable, Sendable { case getAddressList(_ retryStrategy: REST.RetryStrategy) case getRelayList(_ retryStrategy: REST.RetryStrategy, etag: String?) + case sendProblemReport(_ retryStrategy: REST.RetryStrategy, problemReportRequest: ProblemReportRequest) + case createAccount(_ retryStrategy: REST.RetryStrategy) case getAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String) case deleteAccount(_ retryStrategy: REST.RetryStrategy, accountNumber: String) @@ -19,6 +22,8 @@ public enum APIRequest: Codable, Sendable { "get-address-list" case .getRelayList: "get-relay-list" + case .sendProblemReport: + "send-problem-report" case .createAccount: "create-account" case .getAccount: @@ -33,6 +38,7 @@ public enum APIRequest: Codable, Sendable { case let .getAddressList(strategy), let .getRelayList(strategy, _), + let .sendProblemReport(strategy, _), let .createAccount(strategy), let .getAccount(strategy, _), let .deleteAccount(strategy, _): diff --git a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift index 298d3ac31f..0b3271c80b 100644 --- a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift +++ b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift @@ -24,36 +24,44 @@ public struct MullvadApiRequestFactory: Sendable { let rawCompletionPointer = Unmanaged.passRetained(completionPointer).toOpaque() - return switch request { + switch request { case let .getAddressList(retryStrategy): - MullvadApiCancellable(handle: mullvad_api_get_addresses( + return MullvadApiCancellable(handle: mullvad_ios_get_addresses( apiContext.context, rawCompletionPointer, retryStrategy.toRustStrategy() )) case let .getRelayList(retryStrategy, etag: etag): - MullvadApiCancellable(handle: mullvad_api_get_relays( + return MullvadApiCancellable(handle: mullvad_ios_get_relays( apiContext.context, rawCompletionPointer, retryStrategy.toRustStrategy(), etag )) + case let .sendProblemReport(retryStrategy, problemReportRequest): + let rustRequest = RustProblemReportRequest(from: problemReportRequest) + return MullvadApiCancellable(handle: mullvad_ios_send_problem_report( + apiContext.context, + rawCompletionPointer, + retryStrategy.toRustStrategy(), + rustRequest.toRust() + )) case let .getAccount(retryStrategy, accountNumber: accountNumber): - MullvadApiCancellable(handle: mullvad_api_get_account( + return MullvadApiCancellable(handle: mullvad_ios_get_account( apiContext.context, rawCompletionPointer, retryStrategy.toRustStrategy(), accountNumber )) case let .createAccount(retryStrategy): - MullvadApiCancellable(handle: mullvad_api_create_account( + return MullvadApiCancellable(handle: mullvad_ios_create_account( apiContext.context, rawCompletionPointer, retryStrategy.toRustStrategy() )) case let .deleteAccount(retryStrategy, accountNumber: accountNumber): - MullvadApiCancellable(handle: mullvad_api_delete_account( + return MullvadApiCancellable(handle: mullvad_ios_delete_account( apiContext.context, rawCompletionPointer, retryStrategy.toRustStrategy(), diff --git a/ios/MullvadREST/Transport/APITransport.swift b/ios/MullvadREST/Transport/APITransport.swift index 9af1f2779f..d0bf7db48d 100644 --- a/ios/MullvadREST/Transport/APITransport.swift +++ b/ios/MullvadREST/Transport/APITransport.swift @@ -34,7 +34,6 @@ public final class APITransport: APITransportProtocol { let apiRequest = requestFactory.makeRequest(request) return apiRequest { response in - let error: APIError? = if !response.success { APIError( statusCode: Int(response.statusCode), diff --git a/ios/MullvadRustRuntime/RustProblemReportRequest.swift b/ios/MullvadRustRuntime/RustProblemReportRequest.swift new file mode 100644 index 0000000000..677ced4dfb --- /dev/null +++ b/ios/MullvadRustRuntime/RustProblemReportRequest.swift @@ -0,0 +1,48 @@ +// +// RustProblemReportRequest.swift +// MullvadVPN +// +// Created by Mojgan on 2025-03-21. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// +import MullvadLogging +import MullvadTypes + +final public class RustProblemReportRequest { + private let logger = Logger(label: "RustProblemReportRequest") + private let addressPointer: UnsafePointer<CChar>? + private let messagePointer: UnsafePointer<CChar>? + private let logPointer: UnsafePointer<CChar>? + private let problemReportMetaData: ProblemReportMetadata + + public init(from request: ProblemReportRequest) { + self.problemReportMetaData = swift_problem_report_metadata_new() + self.addressPointer = request.address.toCStringPointer() + self.messagePointer = request.message.toCStringPointer() + self.logPointer = request.log.toCStringPointer() + + for (key, value) in request.metadata { + let isAdded = swift_problem_report_metadata_add(problemReportMetaData, key, value) + if !isAdded { + logger + .error("Failed to add metadata. Key: '\(key)' might be invalid or contain unsupported characters.") + } + } + } + + public func toRust() -> SwiftProblemReportRequest { + SwiftProblemReportRequest( + address: addressPointer, + message: messagePointer, + log: logPointer, + metadata: problemReportMetaData + ) + } + + deinit { + swift_problem_report_metadata_free(problemReportMetaData) + addressPointer?.deallocate() + messagePointer?.deallocate() + logPointer?.deallocate() + } +} diff --git a/ios/MullvadRustRuntime/String+UnsafePointer.swift b/ios/MullvadRustRuntime/String+UnsafePointer.swift new file mode 100644 index 0000000000..1cb2017c96 --- /dev/null +++ b/ios/MullvadRustRuntime/String+UnsafePointer.swift @@ -0,0 +1,27 @@ +// +// String+UnsafePointer.swift +// MullvadVPN +// +// Created by Mojgan on 2025-04-02. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension String { + // Ensure the string is converted to a null-terminated C string + // UnsafePointer provides no automated memory management or alignment guarantees. + // The caller is responsible to manage the memory + func toCStringPointer() -> UnsafePointer<CChar>? { + // Convert the Swift string to a null-terminated UTF-8 C string + guard let cString = cString(using: .utf8) else { return nil } + + // Allocate memory for characters + null terminator + let pointer = UnsafeMutablePointer<CChar>.allocate(capacity: cString.count) + + // Copy the characters (including the null terminator) + pointer.initialize(from: cString, count: cString.count) + + return UnsafePointer(pointer) + } +} diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index 12ce0fd4c0..36774f2d4e 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -24,6 +24,8 @@ typedef struct EncryptedDnsProxyState EncryptedDnsProxyState; typedef struct ExchangeCancelToken ExchangeCancelToken; +typedef struct Map Map; + typedef struct RequestCancelHandle RequestCancelHandle; typedef struct RetryStrategy RetryStrategy; @@ -54,6 +56,17 @@ typedef struct CompletionCookie { void *inner; } CompletionCookie; +typedef struct ProblemReportMetadata { + struct Map *inner; +} ProblemReportMetadata; + +typedef struct SwiftProblemReportRequest { + const char *address; + const char *message; + const char *log; + struct ProblemReportMetadata metadata; +} SwiftProblemReportRequest; + typedef struct ProxyHandle { void *context; uint16_t port; @@ -112,7 +125,7 @@ struct SwiftApiContext mullvad_api_init_new(const uint8_t *host, * * This function is not safe to call multiple times with the same `CompletionCookie`. */ -struct SwiftCancelHandle mullvad_api_get_account(struct SwiftApiContext api_context, +struct SwiftCancelHandle mullvad_ios_get_account(struct SwiftApiContext api_context, void *completion_cookie, struct SwiftRetryStrategy retry_strategy, const char *account_number); @@ -129,7 +142,7 @@ struct SwiftCancelHandle mullvad_api_get_account(struct SwiftApiContext api_cont * * This function is not safe to call multiple times with the same `CompletionCookie`. */ -struct SwiftCancelHandle mullvad_api_create_account(struct SwiftApiContext api_context, +struct SwiftCancelHandle mullvad_ios_create_account(struct SwiftApiContext api_context, void *completion_cookie, struct SwiftRetryStrategy retry_strategy); @@ -147,7 +160,7 @@ struct SwiftCancelHandle mullvad_api_create_account(struct SwiftApiContext api_c * * This function is not safe to call multiple times with the same `CompletionCookie`. */ -struct SwiftCancelHandle mullvad_api_delete_account(struct SwiftApiContext api_context, +struct SwiftCancelHandle mullvad_ios_delete_account(struct SwiftApiContext api_context, void *completion_cookie, struct SwiftRetryStrategy retry_strategy, const char *account_number); @@ -164,7 +177,7 @@ struct SwiftCancelHandle mullvad_api_delete_account(struct SwiftApiContext api_c * * This function is not safe to call multiple times with the same `CompletionCookie`. */ -struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_context, +struct SwiftCancelHandle mullvad_ios_get_addresses(struct SwiftApiContext api_context, void *completion_cookie, struct SwiftRetryStrategy retry_strategy); @@ -182,7 +195,7 @@ struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_co * * This function is not safe to call multiple times with the same `CompletionCookie`. */ -struct SwiftCancelHandle mullvad_api_get_relays(struct SwiftApiContext api_context, +struct SwiftCancelHandle mullvad_ios_get_relays(struct SwiftApiContext api_context, void *completion_cookie, struct SwiftRetryStrategy retry_strategy, const char *etag); @@ -225,6 +238,44 @@ extern void mullvad_api_completion_finish(struct SwiftMullvadApiResponse respons struct CompletionCookie completion_cookie); /** + * Send a problem report via the Mullvad API client. + * + * # Safety + * + * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created + * by calling `mullvad_api_init_new`. + * + * This function takes ownership of `completion_cookie`, which must be pointing to a valid instance of Swift + * object `MullvadApiCompletion`. The pointer will be freed by calling `mullvad_api_completion_finish` + * when completion finishes (in completion.finish). + * + * the string properties of `SwiftProblemReportRequest` must be pointers to a null terminated strings. + * + * This function is not safe to call multiple times with the same `CompletionCookie`. + */ +struct SwiftCancelHandle mullvad_ios_send_problem_report(struct SwiftApiContext api_context, + void *completion_cookie, + struct SwiftRetryStrategy retry_strategy, + struct SwiftProblemReportRequest request); + +struct ProblemReportMetadata swift_problem_report_metadata_new(void); + +/** + * Add key and value pair to the `ProblemReportMetadata` + * + * # Safety + * + * `map.inner` must be non-null and point to a valid + * - `key` must be a null-terminated UTF-8 string, containing LF-separated machines. + * - `value` must be a valid pointer to some valid and aligned pointer-sized memory. + */ +bool swift_problem_report_metadata_add(struct ProblemReportMetadata map, + const char *key, + const char *value); + +void swift_problem_report_metadata_free(struct ProblemReportMetadata map); + +/** * Called by the Swift side to signal that the Rust `SwiftMullvadApiResponse` can be safely * dropped from memory. * diff --git a/ios/MullvadTypes/RESTTypes.swift b/ios/MullvadTypes/RESTTypes.swift index 35354a2424..6ba5c08297 100644 --- a/ios/MullvadTypes/RESTTypes.swift +++ b/ios/MullvadTypes/RESTTypes.swift @@ -55,3 +55,17 @@ public struct Device: Codable, Equatable, Sendable { self.ipv6Address = ipv6Address } } + +public struct ProblemReportRequest: Codable, Sendable { + public let address: String + public let message: String + public let log: String + public let metadata: [String: String] + + public init(address: String, message: String, log: String, metadata: [String: String]) { + self.address = address + self.message = message + self.log = log + self.metadata = metadata + } +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 050bd50e1c..b937691e46 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -1017,6 +1017,8 @@ F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */; }; F0A7EBB22CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */; }; F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; + F0A89CB52D9D864B00580C27 /* RustProblemReportRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EEFB9E2D8D60E1007FE4B3 /* RustProblemReportRequest.swift */; }; + F0A89CB72D9D923300580C27 /* String+UnsafePointer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */; }; F0ACE30D2BE4E478006D5333 /* MullvadMockData.h in Headers */ = {isa = PBXBuildFile; fileRef = F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */; settings = {ATTRIBUTES = (Public, ); }; }; F0ACE3102BE4E478006D5333 /* MullvadMockData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; }; F0ACE3112BE4E478006D5333 /* MullvadMockData.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1081,6 +1083,7 @@ F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */; }; F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */; }; F0EF50D52A949F8E0031E8DF /* ChangeLogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */; }; + F0F146942D9462E100BF78E7 /* RustProblemReportRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F146912D94491200BF78E7 /* RustProblemReportRequestTests.swift */; }; F0F316192BF3572B0078DBCF /* RelaySelectorResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */; }; F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */; }; F0F56B092C0E058A009D676B /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; }; @@ -2429,6 +2432,7 @@ F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsStrategyTests.swift; sourceTree = "<group>"; }; F0A163882C47B46300592300 /* SingleHopEphemeralPeerExchangerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleHopEphemeralPeerExchangerTests.swift; sourceTree = "<group>"; }; F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLogTests.swift; sourceTree = "<group>"; }; + F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+UnsafePointer.swift"; sourceTree = "<group>"; }; F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadMockData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadMockData.h; sourceTree = "<group>"; }; F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProxyFactory.swift; sourceTree = "<group>"; }; @@ -2479,7 +2483,9 @@ F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionViewModel.swift; sourceTree = "<group>"; }; F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionViewController.swift; sourceTree = "<group>"; }; F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionInteractor.swift; sourceTree = "<group>"; }; + F0EEFB9E2D8D60E1007FE4B3 /* RustProblemReportRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustProblemReportRequest.swift; sourceTree = "<group>"; }; F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogViewModel.swift; sourceTree = "<group>"; }; + F0F146912D94491200BF78E7 /* RustProblemReportRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustProblemReportRequestTests.swift; sourceTree = "<group>"; }; F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArguments.swift; sourceTree = "<group>"; }; F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorResult.swift; sourceTree = "<group>"; }; F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoRelaysSatisfyingConstraintsError.swift; sourceTree = "<group>"; }; @@ -2740,6 +2746,7 @@ 440E9EF32BDA942E00B1FD11 /* MullvadREST */ = { isa = PBXGroup; children = ( + F0F146932D9462BA00BF78E7 /* MullvadApi */, 7AD63A452CDA665200445268 /* Extensions */, F072D3D02C071A9100906F64 /* Shadowsocks */, 440E9EF42BDA943B00B1FD11 /* ApiHandlers */, @@ -4510,17 +4517,19 @@ A992DA1E2C24709F00DE7CE5 /* MullvadRustRuntime */ = { isa = PBXGroup; children = ( - A9D9A4D32C36E1EA004088DD /* mullvad_rust_runtime.h */, - A992DA1F2C24709F00DE7CE5 /* MullvadRustRuntime.h */, 014449942CA293B100C0C2F2 /* EncryptedDNSProxy.swift */, A948809A2BC9308D0090A44C /* EphemeralPeerExchangeActor.swift */, A9EB4F9C2B7FAB21002A2D7A /* EphemeralPeerNegotiator.swift */, A9A557F42B7E3E5C0017ADA8 /* EphemeralPeerReceiver.swift */, + A9D9A4D32C36E1EA004088DD /* mullvad_rust_runtime.h */, 7A99D3702D56220E00891FF7 /* MullvadApiCancellable.swift */, 7A3215702D392F0B005DF395 /* MullvadApiCompletion.swift */, 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */, 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */, + A992DA1F2C24709F00DE7CE5 /* MullvadRustRuntime.h */, + F0EEFB9E2D8D60E1007FE4B3 /* RustProblemReportRequest.swift */, F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */, + F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */, 584023212A406BF5007B27AC /* TunnelObfuscator.swift */, ); path = MullvadRustRuntime; @@ -4807,6 +4816,14 @@ path = ChangeLog; sourceTree = "<group>"; }; + F0F146932D9462BA00BF78E7 /* MullvadApi */ = { + isa = PBXGroup; + children = ( + F0F146912D94491200BF78E7 /* RustProblemReportRequestTests.swift */, + ); + path = MullvadApi; + sourceTree = "<group>"; + }; F0FA16072D7F03F8007E2546 /* Filter */ = { isa = PBXGroup; children = ( @@ -5991,6 +6008,7 @@ F0D5591F2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */, 7A9F28FC2CA69D0C005F2089 /* DAITASettingsTests.swift in Sources */, A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */, + F0F146942D9462E100BF78E7 /* RustProblemReportRequestTests.swift in Sources */, 7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */, F04413622BA45CE30018A6EE /* CustomListLocationNodeBuilder.swift in Sources */, A9A5FA302ACB05160083449F /* InputTextFormatterTests.swift in Sources */, @@ -6804,8 +6822,10 @@ files = ( A9D9A4B12C36D10E004088DD /* ShadowSocksProxy.swift in Sources */, 014449952CA293B100C0C2F2 /* EncryptedDNSProxy.swift in Sources */, + F0A89CB52D9D864B00580C27 /* RustProblemReportRequest.swift in Sources */, 7AB931242D43C2CA005FCEBA /* MullvadApiContext.swift in Sources */, A9D9A4BB2C36D397004088DD /* EphemeralPeerNegotiator.swift in Sources */, + F0A89CB72D9D923300580C27 /* String+UnsafePointer.swift in Sources */, A9D9A4B22C36D12D004088DD /* TunnelObfuscator.swift in Sources */, 7AB931262D43D22F005FCEBA /* MullvadApiResponse.swift in Sources */, A9173C322C36CCDD00F6A08C /* EphemeralPeerReceiver.swift in Sources */, diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift index 6640473e4a..98746416dc 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift @@ -73,20 +73,17 @@ final class ProblemReportInteractor: @unchecked Sendable { output[entry.key.rawValue] = entry.value } - let request = REST.ProblemReportRequest( + let request = ProblemReportRequest( address: email, message: message, log: logString, metadata: metadataDict ) - _ = self.apiProxy.sendProblemReport( - request, - retryStrategy: .default - ) { result in + _ = self.apiProxy.sendProblemReport(request, retryStrategy: .default, completionHandler: { result in DispatchQueue.main.async { completion(result) } - } + }) } } diff --git a/ios/MullvadVPNTests/MullvadREST/MullvadApi/RustProblemReportRequestTests.swift b/ios/MullvadVPNTests/MullvadREST/MullvadApi/RustProblemReportRequestTests.swift new file mode 100644 index 0000000000..aefcfea931 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/MullvadApi/RustProblemReportRequestTests.swift @@ -0,0 +1,67 @@ +// +// RustProblemReportRequestTests.swift +// MullvadVPN +// +// Created by Mojgan on 2025-03-26. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Testing + +@testable import MullvadRustRuntime +@testable import MullvadTypes + +struct RustProblemReportRequestTests { + @Test( + "Test vaild metadata insertion for SendProblemReport", + arguments: [ + ["key1": "value1"], + ["key2": "value2"], + ["long_key_abcdefghijklmnopqrstuvwxyz": "long_value_1234567890"], + ["special_chars_!@#$%": "special_value_(*&^%)"], + ["": ""], + ] + ) + func testMetadataInsertion(metadata: [String: String]) { + let request = ProblemReportRequest( + address: "127.0.0.1", + message: "Test message", + log: "Log data", + metadata: metadata + ) + let rustRequest = RustProblemReportRequest(from: request) + let rustStruct = rustRequest.toRust() + #expect(rustStruct.metadata != nil, "Metadata should not be for \(metadata)") + } + + @Test("Test invalid metadata insertion for SendProblemReport") + func testInvalidMetadataHandling() { + let invalidMetadata: [[UInt8]: [UInt8]] = [ + [0xC0, 0x80]: [0xC0, 0x80], // // Incomplete UTF-8 byte sequence for key an value + [0x7E]: [0x80], // Valid key , but invalid start byte in UTF-8 + [0xE0, 0x80]: [0xC2, 0x80], // Malformed UTF-8 multibyte sequence for key and valid value + ] + let metadata = swift_problem_report_metadata_new() + for (keyBytes, valueBytes) in invalidMetadata { + keyBytes.withUnsafeBytes { (keyPtr: UnsafeRawBufferPointer) in + valueBytes.withUnsafeBytes { (valuePtr: UnsafeRawBufferPointer) in + guard let keyBaseAddress = keyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self), + let valueBaseAddress = valuePtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return + } + + let result = swift_problem_report_metadata_add( + metadata, + keyBaseAddress, + valueBaseAddress + ) + + #expect( + result == false, + "Metadata with invalid UTF-8 should not be added. Key/Value: [\(keyBytes): \(valueBytes)]" + ) + } + } + } + } +} |
