From b66f84de79680aa71c4da98c05dc596055301481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=BCller-Seydlitz?= Date: Tue, 30 Jul 2024 21:09:58 +0200 Subject: [PATCH] Conformance with URLSessionDelegate, URLSessionTaskDelegate migrate to async/await --- NotificationService/NotificationService.swift | 48 ++++++++-------- .../Sources/OpenHABCore/Util/HTTPClient.swift | 56 ++++++++++++------- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 038aacdc..a030ec68 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -18,23 +18,23 @@ import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? - + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent { var notificationActions: [UNNotificationAction] = [] let userInfo = bestAttemptContent.userInfo - + os_log("didReceive userInfo %{PUBLIC}@", log: .default, type: .info, userInfo) - + if let title = userInfo["title"] as? String { bestAttemptContent.title = title } if let message = userInfo["message"] as? String { bestAttemptContent.body = message } - + // Check if the user has defined custom actions in the payload if let actionsArray = parseActions(userInfo), let category = parseCategory(userInfo) { for actionDict in actionsArray { @@ -56,12 +56,12 @@ class NotificationService: UNNotificationServiceExtension { if !notificationActions.isEmpty { os_log("didReceive registering %{PUBLIC}@ for category %{PUBLIC}@", log: .default, type: .info, notificationActions, category) let notificationCategory = - UNNotificationCategory( - identifier: category, - actions: notificationActions, - intentIdentifiers: [], - options: .customDismissAction - ) + UNNotificationCategory( + identifier: category, + actions: notificationActions, + intentIdentifiers: [], + options: .customDismissAction + ) UNUserNotificationCenter.current().getNotificationCategories { existingCategories in var updatedCategories = existingCategories os_log("handleNotification adding category %{PUBLIC}@", log: .default, type: .info, category) @@ -70,13 +70,13 @@ class NotificationService: UNNotificationServiceExtension { } } } - + // check if there is an attachment to put on the notification // this should be last as we need to wait for media // TODO: we should support relative paths and try the user's openHAB (local,remote) for content if let attachmentURLString = userInfo["media-attachment-url"] as? String { let isItem = attachmentURLString.starts(with: "item:") - + let downloadCompletionHandler: @Sendable (UNNotificationAttachment?) -> Void = { attachment in if let attachment { os_log("handleNotification attaching %{PUBLIC}@", log: .default, type: .info, attachmentURLString) @@ -86,7 +86,7 @@ class NotificationService: UNNotificationServiceExtension { } contentHandler(bestAttemptContent) } - + if isItem { downloadAndAttachItemImage(itemURI: attachmentURLString, completion: downloadCompletionHandler) } else { @@ -97,7 +97,7 @@ class NotificationService: UNNotificationServiceExtension { } } } - + override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. @@ -106,7 +106,7 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } } - + private func parseActions(_ userInfo: [AnyHashable: Any]) -> [[String: String]]? { // Extract actions and convert it from JSON string to an array of dictionaries if let actionsString = userInfo["actions"] as? String, let actionsData = actionsString.data(using: .utf8) { @@ -120,7 +120,7 @@ class NotificationService: UNNotificationServiceExtension { } return nil } - + private func parseCategory(_ userInfo: [AnyHashable: Any]) -> String? { // Extract category from aps dictionary if let aps = userInfo["aps"] as? [String: Any], @@ -129,10 +129,10 @@ class NotificationService: UNNotificationServiceExtension { } return nil } - + private func downloadAndAttachMedia(url: String, completion: @escaping (UNNotificationAttachment?) -> Void) { let client = HTTPClient(username: Preferences.username, password: Preferences.username, alwaysSendBasicAuth: Preferences.alwaysSendCreds) - + let downloadCompletionHandler: @Sendable (URL?, URLResponse?, Error?) -> Void = { (localURL, response, error) in guard let localURL else { os_log("Error downloading media %{PUBLIC}@", log: .default, type: .error, error?.localizedDescription ?? "Unknown error") @@ -147,16 +147,16 @@ class NotificationService: UNNotificationServiceExtension { client.downloadFile(url: url, completionHandler: downloadCompletionHandler) } } - + func downloadAndAttachItemImage(itemURI: String, completion: @escaping (UNNotificationAttachment?) -> Void) { guard let itemURI = URL(string: itemURI), let scheme = itemURI.scheme else { os_log("Could not find scheme %{PUBLIC}@", log: .default, type: .info) completion(nil) return } - + let itemName = String(itemURI.absoluteString.dropFirst(scheme.count + 1)) - + let client = HTTPClient(username: Preferences.username, password: Preferences.password, alwaysSendBasicAuth: Preferences.alwaysSendCreds) client.getItem(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], itemName: itemName) { item, error in guard let item else { @@ -199,16 +199,16 @@ class NotificationService: UNNotificationServiceExtension { completion(nil) } } - + func attachFile(localURL: URL, mimeType: String?, completion: @escaping (UNNotificationAttachment?) -> Void) { do { let fileManager = FileManager.default let tempDirectory = NSTemporaryDirectory() let tempFile = URL(fileURLWithPath: tempDirectory).appendingPathComponent(UUID().uuidString) - + try fileManager.moveItem(at: localURL, to: tempFile) let attachment: UNNotificationAttachment? - + if let mimeType, let utType = UTType(mimeType: mimeType), utType.conforms(to: .data) { diff --git a/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift b/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift index 9bd82403..e7a1cb0c 100644 --- a/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift +++ b/OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift @@ -12,7 +12,7 @@ import Foundation import os.log -public class HTTPClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate { +public class HTTPClient: NSObject { // MARK: - Properties private var session: URLSession! @@ -242,7 +242,6 @@ public class HTTPClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate { request.httpBody = body.data(using: .utf8) request.setValue("text/plain", forHTTPHeaderField: "Content-Type") } - performRequest(request: request, download: download) { result, response, error in if let error { os_log("Error with URL %{public}@ : %{public}@", log: .networking, type: .error, url.absoluteString, error.localizedDescription) @@ -281,58 +280,73 @@ public class HTTPClient: NSObject, URLSessionDelegate, URLSessionTaskDelegate { task.resume() } + @available(watchOS 8.0, *) + @available(iOS 15.0, *) + private func performRequest(request: URLRequest, download: Bool) async throws -> (Any?, URLResponse?) { + var request = request + if alwaysSendBasicAuth { + request.setValue(basicAuthHeader(), forHTTPHeaderField: "Authorization") + } + if download { + return try await session.download(for: request) + } else { + return try await session.data(for: request) + } + } +} + +extension HTTPClient: URLSessionDelegate, URLSessionTaskDelegate { // MARK: - URLSessionDelegate for Client Certificates and Basic Auth - public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - urlSessionInternal(session, task: nil, didReceive: challenge, completionHandler: completionHandler) + public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + await urlSessionInternal(session, task: nil, didReceive: challenge) } - public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - urlSessionInternal(session, task: task, didReceive: challenge, completionHandler: completionHandler) + public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + await urlSessionInternal(session, task: task, didReceive: challenge) } - private func urlSessionInternal(_ session: URLSession, task: URLSessionTask?, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + private func urlSessionInternal(_ session: URLSession, task: URLSessionTask?, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { os_log("URLAuthenticationChallenge: %{public}@", log: .networking, type: .info, challenge.protectionSpace.authenticationMethod) let authenticationMethod = challenge.protectionSpace.authenticationMethod switch authenticationMethod { case NSURLAuthenticationMethodServerTrust: - handleServerTrust(challenge: challenge, completionHandler: completionHandler) + return await handleServerTrust(challenge: challenge) case NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodHTTPBasic: if let task { task.authAttemptCount += 1 if task.authAttemptCount > 1 { - completionHandler(.cancelAuthenticationChallenge, nil) + return (.cancelAuthenticationChallenge, nil) } else { - handleBasicAuth(challenge: challenge, completionHandler: completionHandler) + return await handleBasicAuth(challenge: challenge) } } else { - handleBasicAuth(challenge: challenge, completionHandler: completionHandler) + return await handleBasicAuth(challenge: challenge) } case NSURLAuthenticationMethodClientCertificate: - handleClientCertificateAuth(challenge: challenge, completionHandler: completionHandler) + return await handleClientCertificateAuth(challenge: challenge) default: - completionHandler(.performDefaultHandling, nil) + return (.performDefaultHandling, nil) } } - private func handleServerTrust(challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + private func handleServerTrust(challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { guard let serverTrust = challenge.protectionSpace.serverTrust else { - completionHandler(.performDefaultHandling, nil) - return + return (.performDefaultHandling, nil) } let credential = URLCredential(trust: serverTrust) - completionHandler(.useCredential, credential) + return (.useCredential, credential) } - private func handleBasicAuth(challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + private func handleBasicAuth(challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { let credential = URLCredential(user: username, password: password, persistence: .forSession) - completionHandler(.useCredential, credential) + return (.useCredential, credential) } - private func handleClientCertificateAuth(challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + private func handleClientCertificateAuth(challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { let certificateManager = ClientCertificateManager() let (disposition, credential) = certificateManager.evaluateTrust(with: challenge) - completionHandler(disposition, credential) + return (disposition, credential) } }