Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extension AuthorisationMiddleware: ClientMiddleware {
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)) async throws -> (HTTPResponse, HTTPBody?) {
// Use a mutable copy of request
var newRequest = request
if baseURL.host?.hasSuffix("myopenhab.org") == true || alwaysSendBasicAuth, !username.isEmpty, !password.isEmpty {
if configuration.isCloudConnection || alwaysSendBasicAuth, !username.isEmpty, !password.isEmpty {
newRequest.headerFields[.authorization] = basicAuthHeader(username: username, password: password)
}
let (response, body) = try await next(newRequest, body, baseURL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ public struct ConnectionConfiguration: Hashable, Sendable, Codable, Equatable {
}
}

public extension ConnectionConfiguration {
/// Whether this connection is to an openHAB Cloud instance.
/// Currently determined by the "openHAB Cloud Service" user preference (`supportsNotifications`).
var isCloudConnection: Bool { supportsNotifications }
}

extension ConnectionConfiguration: CustomStringConvertible {
public var description: String {
"url: \(url), user: \(username)"
Expand Down
2 changes: 1 addition & 1 deletion OpenHABCore/Sources/OpenHABCore/Util/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ public final class HTTPClient: NSObject, Sendable {
let password = connectionConfiguration.password
let alwaysSendBasicAuth = connectionConfiguration.alwaysSendBasicAuth

if request.url?.host?.hasSuffix("myopenhab.org") == true || alwaysSendBasicAuth, !username.isEmpty, !password.isEmpty {
if connectionConfiguration.isCloudConnection || alwaysSendBasicAuth, !username.isEmpty, !password.isEmpty {
request.setValue(basicAuthHeader(username: username, password: password), forHTTPHeaderField: "Authorization")
}

Expand Down
29 changes: 27 additions & 2 deletions OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ public enum NetworkStatus: String, Sendable {
public struct ConnectionInfo: Equatable, Sendable {
public let configuration: ConnectionConfiguration
public let version: Int
public let proxyURL: URL?

// Explicit public memberwise initializer
public init(configuration: ConnectionConfiguration, version: Int) {
public init(configuration: ConnectionConfiguration, version: Int, proxyURL: URL? = nil) {
self.configuration = configuration
self.version = version
self.proxyURL = proxyURL
}
}

Expand Down Expand Up @@ -380,6 +382,28 @@ public actor NetworkTracker {
}
}

private func fetchProxyURL(for config: ConnectionConfiguration) async -> URL? {
guard config.isCloudConnection,
let baseURL = URL(string: config.url) else { return nil }
let proxyEndpoint = baseURL.appendingPathComponent("api/v1/proxyurl")
var request = URLRequest(url: proxyEndpoint)
request.timeoutInterval = 5
if !config.username.isEmpty, !config.password.isEmpty {
request.setValue(
basicAuthHeader(username: config.username, password: config.password),
forHTTPHeaderField: "Authorization"
)
}
do {
let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONDecoder().decode([String: String].self, from: data)
if let urlString = json["url"] { return URL(string: urlString) }
} catch {
Logger.networkTracker.info("NetworkTracker: Failed to fetch proxyURL: \(error.localizedDescription)")
}
return nil
}

/// tests connectivity for a given connection, but at most until timeout
private func testConnection(configuration: ConnectionConfiguration) async -> ConnectionInfo? {
guard URL(string: configuration.url) != nil else { return nil }
Expand All @@ -396,7 +420,8 @@ public actor NetworkTracker {
Logger.networkTracker.info("NetworkTracker: testConnection for url: \(configuration.url) user: \(configuration.username, privacy: .private)")
let connection = try await connectionPool.getOrCreateService(for: configuration)
let version = try await connection.getRootVersion()
let connectionInfo = ConnectionInfo(configuration: configuration, version: version)
let proxyURL = await fetchProxyURL(for: configuration)
let connectionInfo = ConnectionInfo(configuration: configuration, version: version, proxyURL: proxyURL)

await failureTracker.reset(configuration) // Reset on success
Logger.networkTracker.info("NetworkTracker: testConnection successful for \(configuration.url)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public final class OpenHABAccessTokenAdapter {

public func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
guard let connectionConfiguration else { return urlRequest }
guard connectionConfiguration.alwaysSendBasicAuth || urlRequest.url?.host?.hasSuffix("myopenhab.org") == true else {
guard connectionConfiguration.alwaysSendBasicAuth || connectionConfiguration.isCloudConnection else {
// The user did not choose for the credentials to be sent with every request.
return urlRequest
}
Expand Down
77 changes: 3 additions & 74 deletions OpenHABCore/Sources/OpenHABCore/Util/SessionChallengeHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public func onReceiveSessionTaskChallenge(with challenge: URLAuthenticationChall
let networkTracker = NetworkTracker.shared
let activeConnection = await networkTracker.activeConnection
guard let configuration = activeConnection?.configuration else { return (.cancelAuthenticationChallenge, credential) }
if challenge.protectionSpace.host == URL(string: configuration.url)?.host || challenge.protectionSpace.host == "home.myopenhab.org" {
let proxyHost = activeConnection?.proxyURL?.host
if challenge.protectionSpace.host == URL(string: configuration.url)?.host
|| challenge.protectionSpace.host == proxyHost {
credential = URLCredential(user: configuration.username, password: configuration.password, persistence: .forSession)
disposition = .useCredential
Logger.sessionChallenge.info(".useCredential")
Expand Down Expand Up @@ -67,76 +69,3 @@ public func onReceiveSessionChallenge(with challenge: URLAuthenticationChallenge
return (disposition, nil)
}
}

final class SessionChallengeHandler {
private let username: String
private let password: String
private let localUrl: URL?
private let remoteUrl: URL?

private let clientCertEvaluator: ((URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
private let serverTrustEvaluator: ((URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?

init(username: String,
password: String,
localUrl: URL?,
remoteUrl: URL?,
serverTrustEvaluator: ((URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))? = nil,
clientCertEvaluator: ((URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))? = nil) {
self.username = username
self.password = password
self.localUrl = localUrl
self.remoteUrl = remoteUrl
self.serverTrustEvaluator = serverTrustEvaluator
self.clientCertEvaluator = clientCertEvaluator
}

func handleSessionTaskChallenge(_ challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) {
Logger.sessionChallengeHandler.debug("SessionTaskChallenge host: \(challenge.protectionSpace.host, privacy: .public)")

if challenge.previousFailureCount > 0 {
return (.cancelAuthenticationChallenge, nil)
}

let authMethod = challenge.protectionSpace.authenticationMethod
if authMethod == NSURLAuthenticationMethodHTTPBasic || authMethod == NSURLAuthenticationMethodDefault {
if isTrustedHost(challenge.protectionSpace.host) {
let credential = URLCredential(user: username, password: password, persistence: .forSession)
Logger.sessionChallengeHandler.debug("Using HTTP BasicAuth for host: \(challenge.protectionSpace.host, privacy: .public)")
return (.useCredential, credential)
}
}

return (.performDefaultHandling, nil)
}

func handleSessionChallenge(_ challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) {
Logger.sessionChallengeHandler.debug("SessionChallenge host: \(challenge.protectionSpace.host, privacy: .public)")

if challenge.previousFailureCount > 0 {
return (.cancelAuthenticationChallenge, nil)
}

switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodServerTrust:
if let serverTrustEvaluator {
return serverTrustEvaluator(challenge)
}
case NSURLAuthenticationMethodClientCertificate:
if let clientCertEvaluator {
return clientCertEvaluator(challenge)
}
default:
// Try using stored credential if available
if let credential = URLCredentialStorage.shared.defaultCredential(for: challenge.protectionSpace) {
return (.useCredential, credential)
}
}

return (.performDefaultHandling, nil)
}

private func isTrustedHost(_ host: String) -> Bool {
host == localUrl?.host || host == remoteUrl?.host || host == "home.myopenhab.org"
}
}
26 changes: 14 additions & 12 deletions openHAB/OpenHABWebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import WebKit
class OpenHABWebViewController: OpenHABViewController {
private var currentTarget = ""
private var openHABTrackedRootUrl = ""
private var activeConfig: ConnectionConfiguration?
private var activeConnectionInfo: ConnectionInfo?
private var activeConfig: ConnectionConfiguration? { activeConnectionInfo?.configuration }
private var hideNavigationBar = false
private var activityIndicator: UIActivityIndicatorView!
private var sseTimer: Timer?
Expand Down Expand Up @@ -106,7 +107,7 @@ class OpenHABWebViewController: OpenHABViewController {
let activeConfiguration = activeConnection.configuration
Logger.viewController.info("OpenHABWebViewController openHAB URL = \(activeConfiguration.url)")
self.openHABTrackedRootUrl = activeConfiguration.url
self.activeConfig = activeConfiguration
self.activeConnectionInfo = activeConnection
self.loadWebView(force: false)
}
}
Expand Down Expand Up @@ -193,9 +194,9 @@ class OpenHABWebViewController: OpenHABViewController {
}

// TODO: remove this check once iOS 16 is dropped
let isMyOh = url?.host?.contains("myopenhab.org") ?? false
let isCloudConnection = activeConfig.isCloudConnection
// create new (or resuse existing)
let newWebview = webView(for: Preferences.shared.currentHomePreferences.id, isMyopenhab: isMyOh)
let newWebview = webView(for: Preferences.shared.currentHomePreferences.id, isCloudConnection: isCloudConnection)
if newWebview != webView {
// Detach old instance
webView.stopLoading()
Expand Down Expand Up @@ -300,8 +301,9 @@ class OpenHABWebViewController: OpenHABViewController {
func modifyUrl(orig: URL?, path: String? = nil) -> URL? {
// better way to clone/copy ?
guard let urlString = orig?.absoluteString, var url = URL(string: urlString) else { return orig }
if url.host == "myopenhab.org" {
url = URL(string: "https://home.myopenhab.org") ?? url
// Use cloud proxy URL if available (resolved from /api/v1/proxyurl)
if let proxyURL = activeConnectionInfo?.proxyURL {
url = proxyURL
}
if let path {
url = appendPathToURL(baseURL: url, path: path) ?? url
Expand Down Expand Up @@ -400,11 +402,11 @@ class OpenHABWebViewController: OpenHABViewController {
}
}

func webView(for id: UUID, isMyopenhab: Bool) -> WKWebView {
func webView(for id: UUID, isCloudConnection: Bool) -> WKWebView {
// TODO: remove all iOS < 17 code when we drop iOS 16 support
if #unavailable(iOS 17) {
if isMyopenhab, let myExsiting = myOhViews[id] {
Logger.viewController.info("Reusing myopenhab webview for id:\(id.uuidString)")
if isCloudConnection, let myExsiting = myOhViews[id] {
Logger.viewController.info("Reusing cloud webview for id:\(id.uuidString)")
return myExsiting
}
}
Expand All @@ -423,8 +425,8 @@ class OpenHABWebViewController: OpenHABViewController {
// iOS 17 allows Sandboxed profiles, which is fantastic, iOS 16 does not and agressively caches everything
if #available(iOS 17, *) {
config.websiteDataStore = WKWebsiteDataStore(forIdentifier: id)
} else if isMyopenhab {
// for myopenhab, create a instance that does not persist or share states (private)
} else if isCloudConnection {
// for cloud connections, create an instance that does not persist or share states (private)
config.websiteDataStore = .nonPersistent()
}

Expand All @@ -449,7 +451,7 @@ class OpenHABWebViewController: OpenHABViewController {
webview.scrollView.scrollIndicatorInsets = .zero

if #unavailable(iOS 17) {
if isMyopenhab {
if isCloudConnection {
myOhViews[id] = webview
return webview
}
Expand Down