diff --git a/Sources/WultraPowerauthNetworking/WPNBaseNetworkingObjects.swift b/Sources/WultraPowerauthNetworking/WPNBaseNetworkingObjects.swift index 5b9abd0..576d71a 100644 --- a/Sources/WultraPowerauthNetworking/WPNBaseNetworkingObjects.swift +++ b/Sources/WultraPowerauthNetworking/WPNBaseNetworkingObjects.swift @@ -126,21 +126,42 @@ public class WPNResponseArray: WPNResponseBase { } } -/// Known values of rest api errors +/// Known values of REST API errors public enum WPNKnownRestApiError: String, Decodable { + // COMMON ERRORS + + /// When unexpected error happened. + case genericError = "ERROR_GENERIC" + /// General authentication failure (wrong password, wrong activation state, etc...) case authenticationFailure = "POWERAUTH_AUTH_FAIL" - /// Failed to register push notifications - case pushRegistrationFailed = "PUSH_REGISTRATION_FAILED" - /// Invalid request sent - missing request object in request case invalidRequest = "INVALID_REQUEST" /// Activation is not valid (it is different from configured activation) case invalidActivation = "INVALID_ACTIVATION" + /// Error during activfation + case activationError = "ERR_ACTIVATION" + + /// Error in case that PowerAuth authentication fails + case authenticationError = "ERR_AUTHENTICATION" + + /// Error during secure vault unlocking + case secureVaultError = "ERR_SECURE_VAULT" + + /// Returned in case encryption or decryption fails + case encryptionError = "ERR_ENCRYPTION" + + // PUSH ERRORS + + /// Failed to register push notifications + case pushRegistrationFailed = "PUSH_REGISTRATION_FAILED" + + // OPERATIONS ERRORS + /// Operation is already finished case operationAlreadyFinished = "OPERATION_ALREADY_FINISHED" @@ -152,6 +173,25 @@ public enum WPNKnownRestApiError: String, Decodable { /// Operation is expired case operationExpired = "OPERATION_EXPIRED" + + // ACTIVATION SPAWN ERRORS + + /// Unable to fetch activation code. + case activationCodeFailed = "ACTIVATION_CODE_FAILED" + + // IDENTITY ONBOARDING ERRORS + + /// Onboarding process failed or failed to start + case onboardingFailed = "ONBOARDING_FAILED" + + /// Failed to resend onboarding OTP (probably requested too soon) + case onboardingOtpFailed = "ONBOARDING_OTP_FAILED" + + /// Document is invalid + case invalidDocument = "INVALID_DOCUMENT" + + /// Identity verification failed + case identityVerificationFailed = "IDENTITY_VERIFICATION_FAILED" } /// Error passed in a response, when the error is returned from an endpoint. diff --git a/Sources/WultraPowerauthNetworking/WPNConfig.swift b/Sources/WultraPowerauthNetworking/WPNConfig.swift index 7f0cb72..f196f47 100644 --- a/Sources/WultraPowerauthNetworking/WPNConfig.swift +++ b/Sources/WultraPowerauthNetworking/WPNConfig.swift @@ -22,8 +22,18 @@ public struct WPNConfig { public let sslValidation: WPNSSLValidationStrategy /// The timeout interval to use when waiting for backend data. + /// + /// Value can be overridden for each `post` call in `WPNNetworkingService`. + /// Default value is 20. public let timeoutIntervalForRequest: TimeInterval + /// Create instance of the config + /// - Parameters: + /// - baseUrl: Base URL for service requests. + /// - sslValidation: SSL validation strategy for the request. + /// - timeoutIntervalForRequest: The timeout interval to use when waiting for backend data. + /// Value can be overridden for each `post` call in `WPNNetworkingService`. + /// Default value is 20. public init(baseUrl: URL, sslValidation: WPNSSLValidationStrategy, timeoutIntervalForRequest: TimeInterval = 20) { self.baseUrl = baseUrl self.sslValidation = sslValidation diff --git a/Sources/WultraPowerauthNetworking/WPNError.swift b/Sources/WultraPowerauthNetworking/WPNError.swift index 67c53a9..e0127aa 100644 --- a/Sources/WultraPowerauthNetworking/WPNError.swift +++ b/Sources/WultraPowerauthNetworking/WPNError.swift @@ -22,33 +22,21 @@ public class WPNError: Error { public init(reason: WPNErrorReason, error: Error? = nil) { #if DEBUG - WPNError.validateNestedError(error) + if let error = error as? WPNError { + D.error("You should not embed WPNError into another WPNError object. Please use .wrap() function if you're not sure what type of error is passed to initializer. Error: \(error.localizedDescription)") + } #endif self.reason = reason self.nestedError = error } /// Private initializer - fileprivate init(reason: WPNErrorReason, - nestedError: Error?, - httpStatusCode: Int, - httpUrlResponse: HTTPURLResponse?, - restApiError: WPNRestApiError?) { - self.nestedError = nestedError - self.reason = reason - self._httpStatusCode = httpStatusCode + fileprivate convenience init(reason: WPNErrorReason, nestedError: Error?, httpUrlResponse: HTTPURLResponse?, restApiError: WPNRestApiError?) { + self.init(reason: reason, error: nestedError) self.httpUrlResponse = httpUrlResponse self.restApiError = restApiError } - #if DEBUG - private static func validateNestedError(_ error: Error?) { - if let error = error as? WPNError { - D.error("You should not embed WPNError into another WPNError object. Please use .wrap() function if you're not sure what type of error is passed to initializer. Error: \(error.localizedDescription)") - } - } - #endif - // MARK: - Properties /// Reason why the error was created @@ -57,62 +45,105 @@ public class WPNError: Error { /// Nested error. public let nestedError: Error? - /// HTTP status code. + /// A full response received from the server. /// - /// If value is not set, then it is automatically gathered from - /// the nested error or from `URLResponse`. Also the nested error must be produced - /// in PowerAuth2 library and contain embedded `PowerAuthRestApiErrorResponse` object. + /// If you set a valid object to this property, then the `httpStatusCode` starts + /// returning status code from the response. You can set this value in cases that, + /// it's important to investigate a whole response, after the authentication fails. /// - /// Due to internal getter optimization, the nested objects evaluation is performed only once. - /// So if you get the value before URL response is set, then the returned value will be incorrect. - /// You can still later override the calculated value by setting a new one. - public var httpStatusCode: Int { + /// Normally, setting `httpStatusCode` is enough for proper handling authentication errors. + internal(set) public var httpUrlResponse: HTTPURLResponse? + + private var _restApiError: WPNRestApiError? // backing field + + /// An optional error describing details about REST API failure. + internal(set) public var restApiError: WPNRestApiError? { get { - if _httpStatusCode >= 0 { - return _httpStatusCode - } else if let httpUrlResponse = httpUrlResponse { - _httpStatusCode = Int(httpUrlResponse.statusCode) - } else if let responseObject = self.powerAuthErrorResponse { - _httpStatusCode = Int(responseObject.httpStatusCode) - } else { - _httpStatusCode = 0 + if let rae = _restApiError { + return rae + } + if let pae = powerAuthRestApiError?.responseObject { + return WPNRestApiError(code: pae.code, message: pae.message) } - return _httpStatusCode + return nil } set { - _httpStatusCode = newValue + _restApiError = newValue } } - /// Private value for httpStatusCode property. - private var _httpStatusCode: Int = -1 + // MARK: - Computed properties - /// A full response received from the server. + /// HTTP status code. /// - /// If you set a valid object to this property, then the `httpStatusCode` starts - /// returning status code from the response. You can set this value in cases that, - /// it's important to investigate a whole response, after the authentication fails. + /// nil if not available (not an HTTP error). + public var httpStatusCode: Int? { + if let httpUrlResponse = httpUrlResponse { + return Int(httpUrlResponse.statusCode) + } else if let responseObject = powerAuthRestApiError { + return Int(responseObject.httpStatusCode) + } else { + return nil + } + } + + /// Returns `PowerAuthRestApiErrorResponse` if such object is embedded in nested error. This is typically useful + /// for getting error HTTP response created in the PowerAuth2 library. + public var powerAuthRestApiError: PowerAuthRestApiErrorResponse? { + return userInfo[PowerAuthErrorDomain] as? PowerAuthRestApiErrorResponse + } + + /// Returns PowerAuth error code when the error was caused by the PowerAuth2 library. /// - /// Normally, setting `httpStatusCode` is enough for proper handling authentication errors. - public var httpUrlResponse: HTTPURLResponse? + /// For possible values, visit [PowerAuth Documentation](https://developers.wultra.com/components/powerauth-mobile-sdk/develop/documentation/PowerAuth-SDK-for-iOS#error-handling) + public var powerAuthErrorCode: PowerAuthErrorCode? { + return (nestedError as NSError?)?.powerAuthErrorCode + } - /// An optional error describing details about REST API failure. - public var restApiError: WPNRestApiError? -} - -// MARK: - Wrapping Error into WPNError - -public extension WPNError { + /// Returns error message when the underlying error was caused by the PowerAuth2 library. + public var powerAuthErrorMessage: String? { + guard domain == PowerAuthErrorDomain else { + return nil + } + return userInfo[NSLocalizedDescriptionKey] as? String + } + + /// Returns true if the error is caused by the missing network connection. + /// The device is typically not connected to the internet. + public var networkIsNotReachable: Bool { + if domain == NSURLErrorDomain || domain == kCFErrorDomainCFNetwork as String { + let ec = CFNetworkErrors(rawValue: Int32(code)) + return ec == .cfurlErrorNotConnectedToInternet || + ec == .cfurlErrorInternationalRoamingOff || + ec == .cfurlErrorDataNotAllowed + } + return false + } + + /// Returns true if the error is related to the connection security - like untrusted TLS + /// certificate, or similar TLS related problems. + public var networkConnectionIsNotTrusted: Bool { + if domain == NSURLErrorDomain || domain == kCFErrorDomainCFNetwork as String { + let code = Int32(code) + if code == CFNetworkErrors.cfurlErrorServerCertificateHasBadDate.rawValue || + code == CFNetworkErrors.cfurlErrorServerCertificateUntrusted.rawValue || + code == CFNetworkErrors.cfurlErrorServerCertificateHasUnknownRoot.rawValue || + code == CFNetworkErrors.cfurlErrorServerCertificateNotYetValid.rawValue || + code == CFNetworkErrors.cfurlErrorSecureConnectionFailed.rawValue { + return true + } + } + return false + } /// Returns WPNError object with nested error and additional nested description. /// If the provided error object is already WPNError, then returns copy of the object, - /// with modiffied nested description. - static func wrap(_ reason: WPNErrorReason, _ error: Error? = nil) -> WPNError { + /// with modified nested description. + public static func wrap(_ reason: WPNErrorReason, _ error: Error? = nil) -> WPNError { if let error = error as? WPNError { return WPNError( reason: reason, nestedError: error.nestedError, - httpStatusCode: error._httpStatusCode, httpUrlResponse: error.httpUrlResponse, restApiError: error.restApiError) } @@ -134,15 +165,8 @@ public struct WPNErrorReason: RawRepresentable, Equatable, Hashable { } } -// MARK: - Computed properties - public extension WPNError { - /// A fallback domain identifier which is returned in situations, when the nested error - /// is not set, or if it's not kind of NSError object. - static let domain = "WPNError" - - /// If nestedError is valid, then returns its code var code: Int { guard let e = nestedError as NSError? else { return 0 @@ -150,75 +174,28 @@ public extension WPNError { return e.code } - /// If nestedError is valid, then returns its domain. - /// Otherwise returns `WPNError.domain` var domain: String { guard let e = nestedError as NSError? else { - return WPNError.domain + return "WPNError" } return e.domain } - /// If nestedError is valid, then returns its user info. var userInfo: [String: Any] { guard let e = nestedError as NSError? else { return [:] } return e.userInfo } - - /// Returns true if nested error has information about missing network connection. - /// The device is typically not connected to the internet. - var networkIsNotReachable: Bool { - if self.domain == NSURLErrorDomain || self.domain == kCFErrorDomainCFNetwork as String { - let ec = CFNetworkErrors(rawValue: Int32(self.code)) - return ec == .cfurlErrorNotConnectedToInternet || - ec == .cfurlErrorInternationalRoamingOff || - ec == .cfurlErrorDataNotAllowed - } - return false - } - - /// Returns true if nested error has information about connection security, like untrusted TLS - /// certificate, or similar TLS related problems. - var networkConnectionIsNotTrusted: Bool { - let domain = self.domain - if domain == NSURLErrorDomain || domain == kCFErrorDomainCFNetwork as String { - let code = Int32(self.code) - if code == CFNetworkErrors.cfurlErrorServerCertificateHasBadDate.rawValue || - code == CFNetworkErrors.cfurlErrorServerCertificateUntrusted.rawValue || - code == CFNetworkErrors.cfurlErrorServerCertificateHasUnknownRoot.rawValue || - code == CFNetworkErrors.cfurlErrorServerCertificateNotYetValid.rawValue || - code == CFNetworkErrors.cfurlErrorSecureConnectionFailed.rawValue { - return true - } - } - return false - } - - /// Returns `PowerAuthRestApiErrorResponse` if such object is embedded in nested error. This is typically useful - /// for getting response created in the PowerAuth2 library. - var powerAuthErrorResponse: PowerAuthRestApiErrorResponse? { - if let responseObject = self.userInfo[PowerAuthErrorDomain] as? PowerAuthRestApiErrorResponse { - return responseObject - } - return nil - } - - var powerAuthRestApiErrorCode: String? { - if let response = restApiError { - return response.code - } - if let code = powerAuthErrorResponse?.responseObject?.code { - return code - } - return nil - } } extension WPNError: CustomStringConvertible { public var description: String { + if let powerAuthErrorMessage = powerAuthErrorMessage { + return powerAuthErrorMessage + } + if let nsne = nestedError as NSError? { return nsne.description } @@ -227,12 +204,12 @@ extension WPNError: CustomStringConvertible { result += "\nError domain: \(domain), code: \(code)" - if httpStatusCode != -1 { + if let httpStatusCode = httpStatusCode { result += "\nHTTP Status Code: \(httpStatusCode)" } - if let raec = powerAuthRestApiErrorCode { - result += "\nPA REST API Code: \(raec)" + if let powerAuthRestApiError = powerAuthRestApiError { + result += "\nPA REST API Code: \(powerAuthRestApiError)" } return result @@ -244,3 +221,13 @@ extension D { D.error(error().description) } } + +/// Simple error class to add developer comment when throwing an WPNError +internal class WPNSimpleError: Error { + + let localizedDescription: String + + init(message: String) { + localizedDescription = message + } +} diff --git a/Sources/WultraPowerauthNetworking/WPNHttpClient.swift b/Sources/WultraPowerauthNetworking/WPNHttpClient.swift index e29cd44..aa91717 100644 --- a/Sources/WultraPowerauthNetworking/WPNHttpClient.swift +++ b/Sources/WultraPowerauthNetworking/WPNHttpClient.swift @@ -39,7 +39,7 @@ class WPNHttpClient: NSObject, URLSessionDelegate { super.init() } - func post(request: WPNHttpRequest, completion: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) { + func post(request: WPNHttpRequest, progressCallback: ((Double) -> Void)?, completion: @escaping (Data?, HTTPURLResponse?, Error?) -> Void) { let urlRequest = request.buildUrlRequest() @@ -49,13 +49,29 @@ class WPNHttpClient: NSObject, URLSessionDelegate { urlRequest.printToConsole() - urlSession.dataTask(with: urlRequest) { responseData, response, error in + var observation: NSKeyValueObservation? + + let task = urlSession.dataTask(with: urlRequest) { responseData, response, error in + observation?.invalidate() + observation = nil assert(Thread.isMainThread) // make sure we're on the right thread let httpResponse = response as? HTTPURLResponse httpResponse?.printToConsole(withData: responseData, andError: error) completion(responseData, httpResponse, error) - }.resume() + } + + if let progressCallback = progressCallback { + if #available(iOS 11.0, tvOS 11.0, *) { + observation = task.progress.observe(\.fractionCompleted) { progress, _ in + progressCallback(progress.fractionCompleted) + } + } else { + // iOS 10 (iPhone 5 and older) + progressCallback(-1) + } + } + task.resume() } // URLSessionDelegate diff --git a/Sources/WultraPowerauthNetworking/WPNHttpRequest.swift b/Sources/WultraPowerauthNetworking/WPNHttpRequest.swift index baca39c..638848e 100644 --- a/Sources/WultraPowerauthNetworking/WPNHttpRequest.swift +++ b/Sources/WultraPowerauthNetworking/WPNHttpRequest.swift @@ -27,14 +27,10 @@ private let jsonDecoder: JSONDecoder = { class WPNHttpRequest { - enum BodyType: String { - case json = "application/json" - } - - /// Default value is `.json` - var requestType = BodyType.json - /// Default value is `.json` - var responseType = BodyType.json + /// Timeout interval of the request. + /// + /// Value from `WPNNetworkingService` `config` will be used when nil. + var timeoutInterval: TimeInterval? private(set) var url: URL private(set) var uriIdentifier: String? @@ -95,7 +91,12 @@ class WPNHttpRequest { var request = URLRequest(url: url) - let requestHeaders = headers.merging(["Accept": responseType.rawValue, "Content-Type": requestType.rawValue], uniquingKeysWith: { f, _ in f }) + if let ti = timeoutInterval { + request.timeoutInterval = ti + } + + let jsonType = "application/json" + let requestHeaders = headers.merging(["Accept": jsonType, "Content-Type": jsonType], uniquingKeysWith: { f, _ in f }) for (k, v) in requestHeaders { request.addValue(v, forHTTPHeaderField: k) @@ -123,40 +124,39 @@ class WPNHttpRequest { /// Builds current request and sets the data to `requestData` property private func buildRequestData(_ request: TRequest) { do { - switch requestType { - case .json: - requestData = try jsonEncoder.encode(request) - } + requestData = try jsonEncoder.encode(request) } catch let error { D.error("failed to build JSON request:\n\(error)") } } /// Parses given result data and sets it to `response` property - func processResult(data: Data) -> TResponse? { - - var data = data - var response: TResponse? + func processResult(data: Data) -> ProcessResultResponse { do { if let encryptor = encryptor { - if let respData = encryptor.decryptResponse(try jsonDecoder.decode(E2EEResponse.self, from: data).toCryptorgram()) { - data = respData + if let decryptedData = encryptor.decryptResponse(try jsonDecoder.decode(E2EEResponse.self, from: data).toCryptorgram()) { + return .encrypted(obj: try jsonDecoder.decode(TResponse.self, from: decryptedData), decryptedData: decryptedData) } else { D.error("failed to decrypt response") + return .failed(error: WPNSimpleError(message: "failed to decrypt response")) } + } else { + return .plain(obj: try jsonDecoder.decode(TResponse.self, from: data)) } - switch responseType { - case .json: - response = try jsonDecoder.decode(TResponse.self, from: data) - } - } catch let error { + } catch { D.error("failed to process result:\n\(error)") + return .failed(error: error) } - return response } } +enum ProcessResultResponse { + case plain(obj: T) + case encrypted(obj: T, decryptedData: Data) + case failed(error: Error) +} + private struct E2EERequest: Encodable { let ephemeralPublicKey: String? let encryptedData: String? diff --git a/Sources/WultraPowerauthNetworking/WPNLogger.swift b/Sources/WultraPowerauthNetworking/WPNLogger.swift index 813a43b..1ee51ac 100644 --- a/Sources/WultraPowerauthNetworking/WPNLogger.swift +++ b/Sources/WultraPowerauthNetworking/WPNLogger.swift @@ -34,11 +34,14 @@ public class WPNLogger { /// Current verbose level. Note that value is ignored for non-DEBUG builds. public static var verboseLevel: VerboseLevel = .warnings + /// Character limit for single log message. Default is 12 000. Unlimited when nil + public static var characterLimit: Int? = 12_000 + /// Prints simple message to the debug console. static func print(_ message: @autoclosure () -> String) { #if DEBUG if verboseLevel == .all { - Swift.print("[WPN] \(message())") + Swift.print("[WPN] \(message().limit(characterLimit))") } #endif } @@ -47,7 +50,7 @@ public class WPNLogger { static func warning(_ message: @autoclosure () -> String) { #if DEBUG if verboseLevel.rawValue >= VerboseLevel.warnings.rawValue { - Swift.print("[WPN] WARNING: \(message())") + Swift.print("[WPN] WARNING: \(message().limit(characterLimit))") } #endif } @@ -56,7 +59,7 @@ public class WPNLogger { static func error(_ message: @autoclosure () -> String) { #if DEBUG if verboseLevel != .off { - Swift.print("[WPN] ERROR: \(message())") + Swift.print("[WPN] ERROR: \(message().limit(characterLimit))") } #endif } @@ -84,4 +87,13 @@ public class WPNLogger { #endif } +private extension String { + func limit(_ characterLimit: Int?) -> String { + guard let cl = characterLimit else { + return self + } + return String(prefix(cl)) + } +} + internal typealias D = WPNLogger diff --git a/Sources/WultraPowerauthNetworking/WPNNetworkingService.swift b/Sources/WultraPowerauthNetworking/WPNNetworkingService.swift index c8446bd..e952652 100644 --- a/Sources/WultraPowerauthNetworking/WPNNetworkingService.swift +++ b/Sources/WultraPowerauthNetworking/WPNNetworkingService.swift @@ -23,20 +23,48 @@ public class WPNNetworkingService { /// Language sent to the server for request localized response. /// - /// Standard accept-language format. Default value is "en". + /// Compliant with standard RFC Accept-Language. Default value is "en". public var acceptLanguage: String - private let powerAuth: PowerAuthSDK + /// Configuration of the service + public let config: WPNConfig + + /// Response delegate is called on each received response + public weak var responseDelegate: WPNResponseDelegate? + + /// Requests that should be signed with the PowerAuth signing will + /// be serialized in the PowerAuth serial queue when the value is `true`. + /// + /// Default value is `true` + /// + /// With this approach, all signed requests across `WPNNetworkingService` instances using the + /// the same PowerAuth instance (and possibly other classes too) will be serialized into + /// the single serial queue. We recommend leaving this option on unless you're managing the + /// serialization of requests yourself. + /// + /// More about this topic can be found in the + /// [PowerAuth documentation](https://developers.wultra.com/components/powerauth-mobile-sdk/develop/documentation/PowerAuth-SDK-for-iOS#request-synchronization) + public var serializeSignedRequests: Bool = true + + /// PowerAuth instance that will be used for this networking. + public let powerAuth: PowerAuthSDK + private let httpClient: WPNHttpClient - private let config: WPNConfig - private let queue = OperationQueue() + private let concurrentQueue = OperationQueue() + /// Creates instance of the `WPNNetworkingService` + /// - Parameters: + /// - powerAuth: PowerAuth instance that will be used for signing. + /// - config: Configuration of the service + /// - serviceName: Name of the service. Will be reflected in the OperationQueue name and logs. + /// - acceptLanguage: Language sent to the server for request localized response. + /// Compliant with standard RFC Accept-Language. Default value is "en". public init(powerAuth: PowerAuthSDK, config: WPNConfig, serviceName: String, acceptLanguage: String = "en") { self.acceptLanguage = acceptLanguage self.powerAuth = powerAuth self.httpClient = WPNHttpClient(sslValidation: config.sslValidation, timeout: config.timeoutIntervalForRequest) self.config = config - queue.name = serviceName + concurrentQueue.name = "\(serviceName)_concurrent" } /// Sends basic request without an authentication @@ -45,6 +73,10 @@ public class WPNNetworkingService { /// - endpoint: Server endpoint. /// - headers: Custom headers to send along. /// - encryptor: Optional encryptor for End to End Encryption. + /// - timeoutInterval: Timeout interval of the request. + /// Value from `config` will be used when nil. + /// - progressCallback: Reports fraction of how much data was already transferred. + /// Note that on iOS 10 it will be called once with value -1. /// - completion: Completion handler /// - Returns: Operation for observation or operation chaining. @discardableResult @@ -52,10 +84,13 @@ public class WPNNetworkingService { to endpoint: Endpoint, with headers: [String: String]? = nil, encryptedWith encryptor: PowerAuthCoreEciesEncryptor? = nil, + timeoutInterval: TimeInterval? = nil, + progressCallback: ((Double) -> Void)? = nil, completion: @escaping Endpoint.Completion) -> Operation { let url = config.buildURL(endpoint.endpointURLPath) let request = Endpoint.Request(url, requestData: data, encryptor: encryptor) - return post(request: request, completion: completion) + request.timeoutInterval = timeoutInterval + return post(request: request, headers: headers, progressCallback: progressCallback, completion: completion) } /// Sends signed request with provided authentication. @@ -65,6 +100,10 @@ public class WPNNetworkingService { /// - endpoint: Server endpoint. /// - headers: Custom headers to send along. /// - encryptor: Optional encryptor for End to End Encryption. + /// - timeoutInterval: Timeout interval of the request. + /// Value from `config` will be used when nil. + /// - progressCallback: Reports fraction of how much data was already transferred. + /// Note that on iOS 10 it will be called once with value -1. /// - completion: Completion handler /// - Returns: Operation for observation or operation chaining. @discardableResult @@ -73,10 +112,13 @@ public class WPNNetworkingService { to endpoint: Endpoint, with headers: [String: String]? = nil, encryptedWith encryptor: PowerAuthCoreEciesEncryptor? = nil, + timeoutInterval: TimeInterval? = nil, + progressCallback: ((Double) -> Void)? = nil, completion: @escaping Endpoint.Completion) -> Operation { let url = config.buildURL(endpoint.endpointURLPath) let request = Endpoint.Request(url, uriId: endpoint.uriId, auth: auth, requestData: data, encryptor: encryptor) - return post(request: request, completion: completion) + request.timeoutInterval = timeoutInterval + return post(request: request, headers: headers, progressCallback: progressCallback, completion: completion) } /// Sends signed request with provided authentication. @@ -86,6 +128,10 @@ public class WPNNetworkingService { /// - endpoint: Server endpoint. /// - headers: Custom headers to send along. /// - encryptor: Optional encryptor for End to End Encryption. + /// - timeoutInterval: Timeout interval of the request. + /// Value from `config` will be used when nil. + /// - progressCallback: Reports fraction of how much data was already transferred. + /// Note that on iOS 10 it will be called once with value -1. /// - completion: Completion handler /// - Returns: Operation for observation or operation chaining. @discardableResult @@ -94,16 +140,20 @@ public class WPNNetworkingService { to endpoint: Endpoint, with headers: [String: String]? = nil, encryptedWith encryptor: PowerAuthCoreEciesEncryptor? = nil, + timeoutInterval: TimeInterval? = nil, + progressCallback: ((Double) -> Void)? = nil, completion: @escaping Endpoint.Completion) -> Operation { let url = config.buildURL(endpoint.endpointURLPath) let request = Endpoint.Request(url, tokenName: endpoint.tokenName, auth: auth, requestData: data, encryptor: encryptor) - return post(request: request, completion: completion) + request.timeoutInterval = timeoutInterval + return post(request: request, headers: headers, progressCallback: progressCallback, completion: completion) } /// Adds a HTTP post request to the request queue. @discardableResult func post>(request: Endpoint.Request, headers: [String: String]? = nil, + progressCallback: ((Double) -> Void)? = nil, completion: @escaping Endpoint.Completion) -> Operation { // Setup default headers request.addHeaders(getDefaultHeaders()) @@ -128,9 +178,9 @@ public class WPNNetworkingService { return } - self.httpClient.post(request: request, completion: { data, urlResponse, error in + self.httpClient.post(request: request, progressCallback: progressCallback, completion: { [weak self] data, urlResponse, error in - guard operation.isCancelled == false else { + guard let self = self, operation.isCancelled == false else { return } @@ -139,8 +189,23 @@ public class WPNNetworkingService { var errorResponse: WPNRestApiError? if let receivedData = data { - // We have a data - if let responseEnvelope = request.processResult(data: receivedData) { + // Process data + let processedResult = request.processResult(data: receivedData) + + var resp: Resp? + + switch processedResult { + case .plain(let envelope): + self.responseDelegate?.responseReceived(from: request.url, statusCode: urlResponse?.statusCode, body: receivedData) + resp = envelope + case .encrypted(let envelope, let decryptedData): + self.responseDelegate?.encryptedResponseReceived(from: request.url, statusCode: urlResponse?.statusCode, body: receivedData, decrypted: decryptedData) + resp = envelope + case .failed: + self.responseDelegate?.responseReceived(from: request.url, statusCode: urlResponse?.statusCode, body: receivedData) + resp = nil + } + if let responseEnvelope = resp { // Valid envelope if responseEnvelope.status == .Ok { // Success exit from block @@ -173,8 +238,20 @@ public class WPNNetworkingService { } } - op.assignCompletionDispatchQueue(.main) - queue.addOperation(op) + + if serializeSignedRequests && request.needsSignature { + // Add operation to the "signing" queue. + if !powerAuth.executeOperation(onSerialQueue: op) { + // Operation wont be added to the queue if there is a missing + // activation in the powerauth instance. + // In such case, cancel the operation and call completion with appropriate error. + op.cancel() + completion(nil, WPNError(reason: .network_signError, error: WPNSimpleError(message: "Failed to execute signed operation - PowerAuth instance without activation."))) + } + } else { + concurrentQueue.addOperation(op) + } + return op } diff --git a/Sources/WultraPowerauthNetworking/WPNResponseDelegate.swift b/Sources/WultraPowerauthNetworking/WPNResponseDelegate.swift new file mode 100644 index 0000000..175f173 --- /dev/null +++ b/Sources/WultraPowerauthNetworking/WPNResponseDelegate.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 Wultra s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions +// and limitations under the License. +// + +import Foundation + +/// Delegate for tapping into the communication and receiving raw data . +public protocol WPNResponseDelegate: AnyObject { + /// Called when response is received + func responseReceived(from url: URL, statusCode: Int?, body: Data) + /// Called when encrypted response is received + func encryptedResponseReceived(from url: URL, statusCode: Int?, body: Data, decrypted: Data) +} diff --git a/WultraPowerAuthNetworking.podspec b/WultraPowerAuthNetworking.podspec index b49a6c7..6a8cc28 100644 --- a/WultraPowerAuthNetworking.podspec +++ b/WultraPowerAuthNetworking.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.cocoapods_version = '>= 1.10' s.name = "WultraPowerAuthNetworking" - s.version = '1.1.0' + s.version = '1.0.3' s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } s.summary = "PowerAuth Networking by Wultra" s.homepage = "https://www.wultra.com/" @@ -14,4 +14,4 @@ Pod::Spec.new do |s| s.ios.deployment_target = '10.0' s.dependency 'PowerAuth2', '>= 1.6' -end \ No newline at end of file +end diff --git a/WultraPowerAuthNetworking.xcodeproj/project.pbxproj b/WultraPowerAuthNetworking.xcodeproj/project.pbxproj index 4821c9f..ded9252 100644 --- a/WultraPowerAuthNetworking.xcodeproj/project.pbxproj +++ b/WultraPowerAuthNetworking.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ BF732288273D3CDE00DE32D7 /* PowerAuthCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF732286273D3CDE00DE32D7 /* PowerAuthCore.xcframework */; }; BF732289273D3CDE00DE32D7 /* PowerAuth2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF732287273D3CDE00DE32D7 /* PowerAuth2.xcframework */; }; + DCCD8F032771F073004D8BF7 /* WPNResponseDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCCD8F022771F073004D8BF7 /* WPNResponseDelegate.swift */; }; OBJ_37 /* WPNAsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* WPNAsyncOperation.swift */; }; OBJ_38 /* WPNBaseNetworkingObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* WPNBaseNetworkingObjects.swift */; }; OBJ_39 /* WPNConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* WPNConfig.swift */; }; @@ -24,6 +25,7 @@ /* Begin PBXFileReference section */ BF732286273D3CDE00DE32D7 /* PowerAuthCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PowerAuthCore.xcframework; path = Carthage/Build/PowerAuthCore.xcframework; sourceTree = ""; }; BF732287273D3CDE00DE32D7 /* PowerAuth2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PowerAuth2.xcframework; path = Carthage/Build/PowerAuth2.xcframework; sourceTree = ""; }; + DCCD8F022771F073004D8BF7 /* WPNResponseDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WPNResponseDelegate.swift; path = Sources/WultraPowerauthNetworking/WPNResponseDelegate.swift; sourceTree = SOURCE_ROOT; }; OBJ_10 /* WPNAsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WPNAsyncOperation.swift; sourceTree = ""; }; OBJ_11 /* WPNBaseNetworkingObjects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WPNBaseNetworkingObjects.swift; sourceTree = ""; }; OBJ_12 /* WPNConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WPNConfig.swift; sourceTree = ""; }; @@ -109,16 +111,17 @@ isa = PBXGroup; children = ( OBJ_9 /* WultraPowerAuthNetworking.h */, + OBJ_18 /* WPNNetworkingService.swift */, + OBJ_15 /* WPNHttpClient.swift */, + OBJ_16 /* WPNHttpRequest.swift */, OBJ_10 /* WPNAsyncOperation.swift */, OBJ_11 /* WPNBaseNetworkingObjects.swift */, OBJ_12 /* WPNConfig.swift */, OBJ_13 /* WPNEndpoint.swift */, OBJ_14 /* WPNError.swift */, - OBJ_15 /* WPNHttpClient.swift */, - OBJ_16 /* WPNHttpRequest.swift */, OBJ_17 /* WPNLogger.swift */, - OBJ_18 /* WPNNetworkingService.swift */, OBJ_19 /* WPNSSLValidationStrategy.swift */, + DCCD8F022771F073004D8BF7 /* WPNResponseDelegate.swift */, ); name = WultraPowerAuthNetworking; path = Sources/WultraPowerAuthNetworking; @@ -133,6 +136,7 @@ buildPhases = ( OBJ_36 /* Sources */, OBJ_47 /* Frameworks */, + DC3EFA482774A55000C30F64 /* SwiftLint */, ); buildRules = ( ); @@ -152,7 +156,7 @@ isa = PBXProject; attributes = { LastSwiftMigration = 1300; - LastUpgradeCheck = 1310; + LastUpgradeCheck = 1320; }; buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "WultraPowerAuthNetworking" */; compatibilityVersion = "Xcode 3.2"; @@ -173,6 +177,27 @@ }; /* End PBXProject section */ +/* Begin PBXShellScriptBuildPhase section */ + DC3EFA482774A55000C30F64 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if ! [ -x \"$(command -v swiftlint)\" ]; then\n echo 'warning: swiftlint is not installed on this computer.' >&2\n exit 0\nfi\n\nswiftlint\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ OBJ_36 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -185,6 +210,7 @@ OBJ_41 /* WPNError.swift in Sources */, OBJ_42 /* WPNHttpClient.swift in Sources */, OBJ_43 /* WPNHttpRequest.swift in Sources */, + DCCD8F032771F073004D8BF7 /* WPNResponseDelegate.swift in Sources */, OBJ_44 /* WPNLogger.swift in Sources */, OBJ_45 /* WPNNetworkingService.swift in Sources */, OBJ_46 /* WPNSSLValidationStrategy.swift in Sources */, diff --git a/WultraPowerAuthNetworking.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/WultraPowerAuthNetworking.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/WultraPowerAuthNetworking.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/WultraPowerAuthNetworking.xcodeproj/xcshareddata/xcschemes/WultraPowerAuthNetworking.xcscheme b/WultraPowerAuthNetworking.xcodeproj/xcshareddata/xcschemes/WultraPowerAuthNetworking.xcscheme index 8b348c2..6f15cbe 100644 --- a/WultraPowerAuthNetworking.xcodeproj/xcshareddata/xcschemes/WultraPowerAuthNetworking.xcscheme +++ b/WultraPowerAuthNetworking.xcodeproj/xcshareddata/xcschemes/WultraPowerAuthNetworking.xcscheme @@ -1,6 +1,6 @@