Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uses new HTTPClient for background fetch jobs #784

Merged
merged 3 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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