diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index ec65400a4..799a2ca97 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -69,9 +69,12 @@ 514C34DB276CE19C00FCAB79 /* ComposeMessageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DA276CE19C00FCAB79 /* ComposeMessageContext.swift */; }; 514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */; }; 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */; }; + 5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */; }; 5168FB0B274F94D300131072 /* MessageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5168FB0A274F94D300131072 /* MessageAttachment.swift */; }; 51775C32270B01C200D7C944 /* PrvKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */; }; 51775C39270C7D2400D7C944 /* StorageMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C38270C7D2400D7C944 /* StorageMethod.swift */; }; + 517C2E302779F90700FECF32 /* MultipartDataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517C2E2F2779F90700FECF32 /* MultipartDataRequest.swift */; }; + 517C2E33277A0C6300FECF32 /* EnterpriseServerApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517C2E32277A0C6300FECF32 /* EnterpriseServerApiError.swift */; }; 5180CB9127356D48001FC7EF /* MessageSubjectNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */; }; 5180CB9327357B67001FC7EF /* RawClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9227357B67001FC7EF /* RawClientConfiguration.swift */; }; 5180CB9527357BB0001FC7EF /* WkdApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9427357BB0001FC7EF /* WkdApi.swift */; }; @@ -494,9 +497,12 @@ 514C34DA276CE19C00FCAB79 /* ComposeMessageContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageContext.swift; sourceTree = ""; }; 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageRecipient.swift; sourceTree = ""; }; 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeMessageService+State.swift"; sourceTree = ""; }; + 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUploadDetails.swift; sourceTree = ""; }; 5168FB0A274F94D300131072 /* MessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachment.swift; sourceTree = ""; }; 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrvKeyInfoTests.swift; sourceTree = ""; }; 51775C38270C7D2400D7C944 /* StorageMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMethod.swift; sourceTree = ""; }; + 517C2E2F2779F90700FECF32 /* MultipartDataRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartDataRequest.swift; sourceTree = ""; }; + 517C2E32277A0C6300FECF32 /* EnterpriseServerApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterpriseServerApiError.swift; sourceTree = ""; }; 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSubjectNode.swift; sourceTree = ""; }; 5180CB9227357B67001FC7EF /* RawClientConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawClientConfiguration.swift; sourceTree = ""; }; 5180CB9427357BB0001FC7EF /* WkdApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WkdApi.swift; sourceTree = ""; }; @@ -1015,6 +1021,16 @@ path = SignIn; sourceTree = ""; }; + 517C2E312779F91300FECF32 /* Models */ = { + isa = PBXGroup; + children = ( + 517C2E32277A0C6300FECF32 /* EnterpriseServerApiError.swift */, + 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */, + 517C2E2F2779F90700FECF32 /* MultipartDataRequest.swift */, + ); + path = Models; + sourceTree = ""; + }; 51B0C772272AB5FD00124663 /* Extensions */ = { isa = PBXGroup; children = ( @@ -1629,6 +1645,7 @@ children = ( 32DCAC9C0512037018F434A1 /* BackendApi.swift */, 21C7DF08266C0D8F00C44800 /* EnterpriseServerApi.swift */, + 517C2E312779F91300FECF32 /* Models */, ); path = "Account Server Services"; sourceTree = ""; @@ -2573,6 +2590,7 @@ D2E26F6C24F25B1F00612AF1 /* KeyAlgo.swift in Sources */, D2891AC424C62446008918E3 /* ErrorHandler.swift in Sources */, 51938DC1274CC291007AD57B /* MessageQuoteType.swift in Sources */, + 5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */, 9F41FA2F253B7624003B970D /* BackupSelectKeyDecorator.swift in Sources */, D227C0E8250538A90070F805 /* FoldersService.swift in Sources */, 5ADEDCB623A426E300EC495E /* KeyDetailViewController.swift in Sources */, @@ -2692,6 +2710,7 @@ 9FF0671025520D7100FCC9E6 /* MessageGateway.swift in Sources */, 9F31AB8C23298B3F00CF87EA /* Imap+retry.swift in Sources */, 51B4AE51271444580001F33B /* PubKeyRealmObject.swift in Sources */, + 517C2E302779F90700FECF32 /* MultipartDataRequest.swift in Sources */, 9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */, D227C0E3250538100070F805 /* LocalFoldersProvider.swift in Sources */, 9FA405C7265AEBA50084D133 /* SetupGenerateKeyViewController.swift in Sources */, @@ -2700,6 +2719,7 @@ 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */, 606FE33A2745AA2E009DA039 /* AttachmentViewController.swift in Sources */, 21489B78267CB42400BDE4AC /* LocalClientConfiguration.swift in Sources */, + 517C2E33277A0C6300FECF32 /* EnterpriseServerApiError.swift in Sources */, 9FF0671C25520D9D00FCC9E6 /* MailProvider.swift in Sources */, 9FE743072347AA54005E2DBB /* MainNavigationController.swift in Sources */, 9F5C2A8B257E6C4900DE9B4B /* ImapError.swift in Sources */, diff --git a/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift b/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift index 8a1cca0fb..8a510a62e 100644 --- a/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift +++ b/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift @@ -12,20 +12,8 @@ import FlowCryptCommon protocol EnterpriseServerApiType { func getActiveFesUrl(for email: String) async throws -> String? func getClientConfiguration(for email: String) async throws -> RawClientConfiguration -} - -enum EnterpriseServerApiError: Error { - case parse - case emailFormat -} - -extension EnterpriseServerApiError: LocalizedError { - var errorDescription: String? { - switch self { - case .parse: return "organisational_rules_parse_error_description".localized - case .emailFormat: return "organisational_rules_email_format_error_description".localized - } - } + func getReplyToken(for email: String) async throws -> String + func upload(message: Data, details: MessageUploadDetails) async throws -> String } /// server run by individual enterprise customers, serves client configuration @@ -37,7 +25,7 @@ class EnterpriseServerApi: EnterpriseServerApiType { private enum Constants { /// 404 - Not Found static let getToleratedHTTPStatuses = [404] - /// -1001 - request timed out, -1003 - сannot resolve host, -1004 - can't conenct to hosts, + /// -1001 - request timed out, -1003 - сannot resolve host, -1004 - can't connect to hosts, /// -1005 - network connection lost, -1006 - dns lookup failed, -1007 - too many redirects /// -1008 - resource unavailable static let getToleratedNSErrorCodes = [-1001, -1003, -1004, -1005, -1006, -1007, -1008] @@ -53,6 +41,20 @@ class EnterpriseServerApi: EnterpriseServerApiType { let clientConfiguration: RawClientConfiguration } + private struct MessageReplyTokenResponse: Decodable { + let replyToken: String + } + + private struct MessageUploadResponse: Decodable { + let url: String + } + + private lazy var decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + }() + private func constructUrlBase(emailDomain: String) -> String { guard !CommandLine.isDebugBundleWithArgument("--mock-fes-api") else { return "http://127.0.0.1:8001/fes" // mock @@ -99,27 +101,65 @@ class EnterpriseServerApi: EnterpriseServerApiType { throw EnterpriseServerApiError.emailFormat } - guard let fesUrl = try await getActiveFesUrl(for: email) else { - return .empty - } + let response: ClientConfigurationResponse = try await performRequest( + email: email, + url: "/api/v1/client-configuration?domain=\(userDomain)", + method: .get, + withAuthorization: false + ) - let request = ApiCall.Request( - apiName: Constants.apiName, - url: "\(fesUrl)/api/v1/client-configuration?domain=\(userDomain)" + return response.clientConfiguration + } + + func getReplyToken(for email: String) async throws -> String { + let response: MessageReplyTokenResponse = try await performRequest( + email: email, + url: "/api/v1/message/new-reply-token" ) - let safeReponse = try await ApiCall.call(request) - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase + return response.replyToken + } - guard let clientConfiguration = (try? decoder.decode( - ClientConfigurationResponse.self, - from: safeReponse.data - ))?.clientConfiguration - else { - throw EnterpriseServerApiError.parse - } - return clientConfiguration + func upload(message: Data, details: MessageUploadDetails) async throws -> String { + let detailsData = try details.toJsonData() + + let detailsDataItem = MultipartDataItem( + data: detailsData, + name: "details", + contentType: "application/json" + ) + let contentDataItem = MultipartDataItem( + data: message, + name: "content", + contentType: "application/octet-stream" + ) + + let request = MultipartDataRequest(items: [detailsDataItem, contentDataItem]) + + let contentTypeHeader = URLHeader( + value: "multipart/form-data; boundary=\(request.boundary)", + httpHeaderField: "Content-Type" + ) + + let response: MessageUploadResponse = try await performRequest( + email: details.from, + url: "/api/v1/message", + headers: [contentTypeHeader], + method: .post, + body: request.httpBody as Data + ) + + return response.url + } + + // MARK: - Helpers + private func getIdToken(email: String) async throws -> String { + let googleService = GoogleUserService( + currentUserEmail: email, + appDelegateGoogleSessionContainer: nil + ) + + return try await googleService.getCachedOrRefreshedIdToken() } private func shouldTolerateWhenCallingOpportunistically(_ error: Error) -> Bool { @@ -132,4 +172,39 @@ class EnterpriseServerApi: EnterpriseServerApiType { } return true } + + private func performRequest( + email: String, + url: String, + headers: [URLHeader] = [], + method: HTTPMethod = .post, + body: Data? = nil, + withAuthorization: Bool = true + ) async throws -> T { + guard let fesUrl = try await getActiveFesUrl(for: email) else { + throw EnterpriseServerApiError.noActiveFesUrl + } + + if withAuthorization { + let idToken = try await getIdToken(email: email) + let authorizationHeader = URLHeader(value: "Bearer \(idToken)", httpHeaderField: "Authorization") + var headers = headers + headers.append(authorizationHeader) + } + + let request = ApiCall.Request( + apiName: Constants.apiName, + url: "\(fesUrl)\(url)", + method: method, + body: body, + headers: headers + ) + + let safeResponse = try await ApiCall.call(request) + + guard let data = try? decoder.decode(T.self, from: safeResponse.data) + else { throw EnterpriseServerApiError.parse } + + return data + } } diff --git a/FlowCrypt/Functionality/Services/Account Server Services/Models/EnterpriseServerApiError.swift b/FlowCrypt/Functionality/Services/Account Server Services/Models/EnterpriseServerApiError.swift new file mode 100644 index 000000000..d2be5b7d1 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Account Server Services/Models/EnterpriseServerApiError.swift @@ -0,0 +1,25 @@ +// +// EnterpriseServerApiError.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 27/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +enum EnterpriseServerApiError: Error { + case parse + case emailFormat + case noActiveFesUrl +} + +extension EnterpriseServerApiError: LocalizedError { + var errorDescription: String? { + switch self { + case .parse: return "organisational_rules_parse_error_description".localized + case .emailFormat: return "organisational_rules_email_format_error_description".localized + case .noActiveFesUrl: return "organisational_rules_fes_url_error_description".localized + } + } +} diff --git a/FlowCrypt/Functionality/Services/Account Server Services/Models/MessageUploadDetails.swift b/FlowCrypt/Functionality/Services/Account Server Services/Models/MessageUploadDetails.swift new file mode 100644 index 000000000..c2613a638 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Account Server Services/Models/MessageUploadDetails.swift @@ -0,0 +1,17 @@ +// +// MessageUploadDetails.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 30/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +struct MessageUploadDetails: Encodable { + let associateReplyToken: String + let from: String + let to: [String] + let cc: [String] + let bcc: [String] +} diff --git a/FlowCrypt/Functionality/Services/Account Server Services/Models/MultipartDataRequest.swift b/FlowCrypt/Functionality/Services/Account Server Services/Models/MultipartDataRequest.swift new file mode 100644 index 000000000..030d54daf --- /dev/null +++ b/FlowCrypt/Functionality/Services/Account Server Services/Models/MultipartDataRequest.swift @@ -0,0 +1,39 @@ +// +// MultipartDataRequest.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 27/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +struct MultipartDataRequest { + private let lineSeparator = "\r\n" + + let boundary = UUID().uuidString + let httpBody = NSMutableData() + + init(items: [MultipartDataItem]) { + items.forEach(append) + appendBoundary() + } + + private func append(item: MultipartDataItem) { + httpBody.append("--\(boundary)\(lineSeparator)") + httpBody.append("Content-Disposition: form-data; name=\"\(item.name)\"; filename=\"\(item.name)\"\(lineSeparator)") + httpBody.append("Content-Type: \(item.contentType)\(lineSeparator)\(lineSeparator)") + httpBody.append(item.data) + httpBody.append(lineSeparator) + } + + private func appendBoundary() { + httpBody.append("--\(boundary)--\(lineSeparator)") + } +} + +struct MultipartDataItem { + let data: Data + let name: String + let contentType: String +} diff --git a/FlowCrypt/Functionality/Services/ApiCall.swift b/FlowCrypt/Functionality/Services/ApiCall.swift index 4955ddf5f..aaa17455f 100644 --- a/FlowCrypt/Functionality/Services/ApiCall.swift +++ b/FlowCrypt/Functionality/Services/ApiCall.swift @@ -15,7 +15,7 @@ extension ApiCall { struct Request { var apiName: String var url: String - var method: HTTPMetod = .get + var method: HTTPMethod = .get var body: Data? var headers: [URLHeader] = [] var timeout: TimeInterval = 60.0 diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift index 12361b553..312d5ff9d 100644 --- a/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift @@ -73,7 +73,7 @@ extension AttesterApi { @discardableResult func update(email: String, pubkey: String, token: String?) async throws -> String { - let httpMethod: HTTPMetod + let httpMethod: HTTPMethod let headers: [URLHeader] if let value = token { httpMethod = .post diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index ca9c767b6..ed3964ffd 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -243,6 +243,7 @@ // Organisational rules error "organisational_rules_parse_error_description" = "Couldn't parse data while getting organisational rules"; "organisational_rules_email_format_error_description" = "Wrong user email format"; +"organisational_rules_fes_url_error_description" = "Couldn't get active FES url"; "organisational_rules_url_not_valid" = "Please check if key manager url set correctly"; "organisational_rules_autoimport_or_autogen_with_private_key_manager_error" = "Combination of rules (key_manager_url set but PRV_AUTOIMPORT_OR_AUTOGEN is missing) is not supported on this platform"; diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift index b033b3c59..4f90e55d4 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/EnterpriseServerApiMock.swift @@ -53,4 +53,12 @@ final class EnterpriseServerApiMock: EnterpriseServerApiType { getClientConfigurationForCurrentUserCount += 1 return try getClientConfigurationForCurrentUserCall() } + + func getReplyToken(for email: String) async throws -> String { + return "" + } + + func upload(message: Data, details: MessageUploadDetails) async throws -> String { + return "" + } } diff --git a/FlowCryptCommon/Extensions/Data/DataExtensions.swift b/FlowCryptCommon/Extensions/Data/DataExtensions.swift index bf89f775f..62c761272 100644 --- a/FlowCryptCommon/Extensions/Data/DataExtensions.swift +++ b/FlowCryptCommon/Extensions/Data/DataExtensions.swift @@ -21,6 +21,13 @@ public extension Data { } } +public extension NSMutableData { + func append(_ string: String) { + guard let data = string.data(using: .utf8) else { return } + self.append(data) + } +} + extension String { init(data: Data) { self = String(decoding: data, as: UTF8.self) diff --git a/FlowCryptCommon/Extensions/URLSessionExtensions.swift b/FlowCryptCommon/Extensions/URLSessionExtensions.swift index 6770a8106..3ebc8fdac 100644 --- a/FlowCryptCommon/Extensions/URLSessionExtensions.swift +++ b/FlowCryptCommon/Extensions/URLSessionExtensions.swift @@ -26,7 +26,7 @@ public struct HttpErr: Error { } } -public enum HTTPMetod: String { +public enum HTTPMethod: String { case put = "PUT" case get = "GET" case post = "POST" @@ -88,7 +88,7 @@ public struct URLHeader { public extension URLRequest { static func urlRequest( with url: URL, - method: HTTPMetod = .get, + method: HTTPMethod = .get, body: Data? = nil, headers: [URLHeader] = [] ) -> URLRequest {