Skip to content

Commit

Permalink
Conformance with URLSessionDelegate, URLSessionTaskDelegate migrate t…
Browse files Browse the repository at this point in the history
…o async/await
  • Loading branch information
timbms committed Jul 30, 2024
1 parent 111e39b commit b66f84d
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 45 deletions.
48 changes: 24 additions & 24 deletions NotificationService/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -86,7 +86,7 @@ class NotificationService: UNNotificationServiceExtension {
}
contentHandler(bestAttemptContent)
}

if isItem {
downloadAndAttachItemImage(itemURI: attachmentURLString, completion: downloadCompletionHandler)
} else {
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -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],
Expand All @@ -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")
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
56 changes: 35 additions & 21 deletions OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}

Expand Down

0 comments on commit b66f84d

Please sign in to comment.