Skip to content

Commit

Permalink
Uses new HTTPClient for background fetch jobs (#784)
Browse files Browse the repository at this point in the history
* Uses new HTTPClient for background fetch jobs
See #783
Signed-off-by: Dan Cunningham <dan@digitaldan.com>

* Use new HTTPClient class for downloading all data in notifications

Signed-off-by: Dan Cunningham <dan@digitaldan.com>

* move download media to new client

Signed-off-by: Dan Cunningham <dan@digitaldan.com>

---------

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
  • Loading branch information
digitaldan authored Jul 7, 2024
1 parent 3e8598b commit 343e007
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 17 deletions.
20 changes: 9 additions & 11 deletions NotificationService/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class NotificationService: UNNotificationServiceExtension {
} else {
downloadAndAttachMedia
}

os_log("handleNotification downloading %{PUBLIC}@", log: .default, type: .info, attachmentURLString)
downloadHandler(attachmentURL) { attachment in
if let attachment {
os_log("handleNotification attaching %{PUBLIC}@", log: .default, type: .info, attachmentURLString)
Expand Down Expand Up @@ -138,26 +138,28 @@ class NotificationService: UNNotificationServiceExtension {
}

private func downloadAndAttachMedia(url: URL, completion: @escaping (UNNotificationAttachment?) -> Void) {
let task = URLSession.shared.downloadTask(with: url) { localURL, response, error in
let client = HTTPClient(username: Preferences.username, password: Preferences.username) // lets not always send auth with this
client.downloadFile(url: url) { localURL, response, error in
guard let localURL else {
os_log("Error downloading media %{PUBLIC}@", log: .default, type: .error, error?.localizedDescription ?? "Unknown error")
completion(nil)
return
}
self.attachFile(localURL: localURL, mimeType: response?.mimeType, completion: completion)
}
task.resume()
}

func downloadAndAttachItemImage(attachmentURL: URL, completion: @escaping (UNNotificationAttachment?) -> Void) {
guard let scheme = attachmentURL.scheme else {
os_log("Could not find scheme %{PUBLIC}@", log: .default, type: .info)
completion(nil)
return
}

let itemName = String(attachmentURL.absoluteString.dropFirst(scheme.count + 1))

OpenHABItemCache.instance.getItem(name: itemName) { item in
let client = HTTPClient(username: Preferences.username, password: Preferences.username, alwaysSendBasicAuth: Preferences.alwaysSendCreds)
client.getItem(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], itemName: itemName) { item, error in
guard let item else {
os_log("Could not find item %{PUBLIC}@", log: .default, type: .info, itemName)
completion(nil)
Expand All @@ -168,11 +170,9 @@ class NotificationService: UNNotificationServiceExtension {
// Extract MIME type and base64 string
let pattern = "^data:(.*?);base64,(.*)$"
let regex = try NSRegularExpression(pattern: pattern, options: [])

if let match = regex.firstMatch(in: state, options: [], range: NSRange(location: 0, length: state.utf16.count)) {
let mimeTypeRange = Range(match.range(at: 1), in: state)
let base64Range = Range(match.range(at: 2), in: state)

if let mimeTypeRange, let base64Range {
let mimeType = String(state[mimeTypeRange])
let base64String = String(state[base64Range])
Expand All @@ -196,10 +196,8 @@ class NotificationService: UNNotificationServiceExtension {
} catch {
os_log("Failed to parse data: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription)
}
completion(nil)
} else {
completion(nil)
}
completion(nil)
}
}

Expand All @@ -222,11 +220,11 @@ class NotificationService: UNNotificationServiceExtension {
os_log("Unrecognized MIME type or file extension", log: .default, type: .error)
attachment = nil
}

completion(attachment)
return
} catch {
os_log("Failed to create UNNotificationAttachment: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription)
completion(nil)
}
completion(nil)
}
}
202 changes: 202 additions & 0 deletions OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Copyright (c) 2010-2024 Contributors to the openHAB project
//
// See the NOTICE file(s) distributed with this work for additional
// information.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0
//
// SPDX-License-Identifier: EPL-2.0

import Foundation
import os.log

public class HTTPClient: NSObject, URLSessionDelegate {
// MARK: - Properties

private var session: URLSession!
private let username: String
private let password: String
private let certManager: ClientCertificateManager
private let alwaysSendBasicAuth: Bool

public init(username: String, password: String, alwaysSendBasicAuth: Bool = false) {
self.username = username
self.password = password
certManager = ClientCertificateManager()
self.alwaysSendBasicAuth = alwaysSendBasicAuth
super.init()

let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60

session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}

// MARK: - URLSessionDelegate for Client Certificates

public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
let serverDistinguishedNames = challenge.protectionSpace.distinguishedNames
let identity = certManager.evaluateTrust(distinguishedNames: serverDistinguishedNames ?? [])

if let identity {
let credential = URLCredential(identity: identity, certificates: nil, persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
let serverTrust = challenge.protectionSpace.serverTrust!
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
let credential = URLCredential(user: username, password: password, persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}

public func doGet(baseURLs: [String], path: String?, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
doRequest(baseURLs: baseURLs, path: path, method: "GET", completion: completion)
}

public func doPost(baseURLs: [String], path: String?, body: String, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
doRequest(baseURLs: baseURLs, path: path, method: "POST", body: body, completion: completion)
}

public func doPut(baseURLs: [String], path: String?, body: String, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
doRequest(baseURLs: baseURLs, path: path, method: "PUT", body: body, completion: completion)
}

public func getItems(baseURLs: [String], completion: @escaping ([OpenHABItem]?, Error?) -> Void) {
doGet(baseURLs: baseURLs, path: "/rest/items") { data, _, error in
if let error {
completion(nil, error)
} else {
do {
var items = [OpenHABItem]()
if let data {
os_log("getItemsInternal Data: %{public}@", log: .networking, type: .debug, String(data: data, encoding: .utf8) ?? "")
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)

// if we are hitting an item, then its OpenHABItem.CodingData] not [OpenHABItem.CodingData]
let codingDatas = try data.decoded(as: [OpenHABItem.CodingData].self, using: decoder)
for codingDatum in codingDatas where codingDatum.openHABItem.type != OpenHABItem.ItemType.group {
items.append(codingDatum.openHABItem)
}
os_log("Loaded items to cache: %{PUBLIC}d", log: .networking, type: .info, items.count)
}
completion(items, nil)
} catch {
os_log("getItemsInternal ERROR: %{PUBLIC}@", log: .networking, type: .info, String(describing: error))
completion(nil, error)
}
}
}
}

public func getItem(baseURLs: [String], itemName: String, completion: @escaping (OpenHABItem?, Error?) -> Void) {
os_log("getItem from URsL %{public}@ and item %{public}@", log: .networking, type: .info, baseURLs, itemName)
doGet(baseURLs: baseURLs, path: "/rest/items/\(itemName)") { data, _, error in
if let error {
completion(nil, error)
} else {
do {
if let data {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
let item = try data.decoded(as: OpenHABItem.CodingData.self, using: decoder)
completion(item.openHABItem, nil)
} else {
completion(nil, NSError(domain: "HTTPClient", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data for item"]))
}
} catch {
os_log("getItemsInternal ERROR: %{PUBLIC}@", log: .networking, type: .info, String(describing: error))
completion(nil, error)
}
}
}
}

public func downloadFile(url: URL, completionHandler: @escaping @Sendable (URL?, URLResponse?, (any Error)?) -> Void) {
let task = session.downloadTask(with: url, completionHandler: completionHandler)
task.resume()
}

// MARK: - Private Methods

// MARK: - Basic Authentication

private func basicAuthHeader() -> String {
let authString = "\(username):\(password)"
let authData = authString.data(using: .utf8)!
return "Basic \(authData.base64EncodedString())"
}

// Perform an HTTP request
private func performRequest(request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
var request = request
if alwaysSendBasicAuth {
request.setValue(basicAuthHeader(), forHTTPHeaderField: "Authorization")
}
let task = session.dataTask(with: request, completionHandler: completion)
task.resume()
}

// General function to perform HTTP requests
private func doRequest(baseURLs: [String], path: String?, method: String, body: String? = nil, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
var urls: [URL] = []
for urlString in baseURLs {
if var urlComponent = URLComponents(string: urlString) {
if let path {
urlComponent.path = path
}
if let url = urlComponent.url {
urls.append(url)
}
}
}

func sendRequest() {
guard !urls.isEmpty else {
os_log("All URLs processed and failed.", log: .networking, type: .error)
completion(nil, nil, NSError(domain: "HTTPClient", code: -1, userInfo: [NSLocalizedDescriptionKey: "All URLs processed and failed."]))
return
}

let url = urls.removeFirst()
var request = URLRequest(url: url)
request.httpMethod = method
if let body {
request.httpBody = body.data(using: .utf8)!
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
}

performRequest(request: request) { data, response, error in
if let error {
os_log("Error with URL %{public}@ : %{public}@", log: .networking, type: .error, url.absoluteString, error.localizedDescription)
// Try the next URL
sendRequest()
} else if let response = response as? HTTPURLResponse {
if (400 ... 599).contains(response.statusCode) {
os_log("HTTP error from URL %{public}@ : %{public}d", log: .networking, type: .error, url.absoluteString, response.statusCode)
// Try the next URL
sendRequest()
} else {
os_log("Response from URL %{public}@ : %{public}d", log: .networking, type: .info, url.absoluteString, response.statusCode)
if let data {
os_log("Data: %{public}@", log: .networking, type: .debug, String(data: data, encoding: .utf8) ?? "")
}
completion(data, response, nil)
}
}
}
}
sendRequest()
}
}
3 changes: 3 additions & 0 deletions OpenHABCore/Sources/OpenHABCore/Util/OSLogExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ public extension OSLog {

/// Logs WkWebView events
static let wkwebview = OSLog(subsystem: subsystem, category: "wkwebview")

/// Log Networking events
static let networking = OSLog(subsystem: subsystem, category: "networking")
}
2 changes: 1 addition & 1 deletion openHAB/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
var userInfo = response.notification.request.content.userInfo
let actionIdentifier = response.actionIdentifier
os_log("Notification clicked: action %{PUBLIC}@ userInfo %{PUBLIC}@", log: .notifications, type: .info, actionIdentifier, userInfo)
os_log("Notification clicked: action %{public}@ userInfo %{public}@", log: .notifications, type: .info, actionIdentifier, userInfo)
if actionIdentifier != UNNotificationDismissActionIdentifier {
if actionIdentifier != UNNotificationDefaultActionIdentifier {
userInfo["actionIdentifier"] = actionIdentifier
Expand Down
14 changes: 9 additions & 5 deletions openHAB/OpenHABRootViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,16 @@ class OpenHABRootViewController: UIViewController {
if components.count == 2 {
let itemName = String(components[0])
let itemCommand = String(components[1])
OpenHABItemCache.instance.getItem(name: itemName) { item in
guard let item else {
os_log("Could not find item %{PUBLIC}@", log: .notifications, type: .info, itemName)
return
let client = HTTPClient(username: Preferences.username, password: Preferences.username)
client.doPost(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], path: "/rest/items/\(itemName)", body: itemCommand) { data, _, error in
if let error {
os_log("Could not send data %{public}@", log: .default, type: .error, error.localizedDescription)
} else {
os_log("Request succeeded", log: .default, type: .info)
if let data {
os_log("Data: %{public}@", log: .default, type: .debug, String(data: data, encoding: .utf8) ?? "")
}
}
OpenHABItemCache.instance.sendCommand(item, commandToSend: itemCommand)
}
}
}
Expand Down

0 comments on commit 343e007

Please sign in to comment.