mirror of
https://github.com/home-assistant/iOS.git
synced 2026-06-16 13:26:27 -05:00
<!-- Thank you for submitting a Pull Request and helping to improve Home Assistant. Please complete the following sections to help the processing and review of your changes. Please do not delete anything from this template. --> ## Summary <!-- Provide a brief summary of the changes you have made and most importantly what they aim to achieve --> In case the mdns discovery starts the onboarding and suddenly the onboarded url gets redirected to a different port or scheme, we are not catching that and updating the onboarded URL to match that, more information on the upper level task. https://github.com/home-assistant/iOS/issues/4724 ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> ## Link to pull request in Documentation repository <!-- Pull requests that add, change or remove functionality must have a corresponding pull request in the Companion App Documentation repository (https://github.com/home-assistant/companion.home-assistant). Please add the number of this pull request after the "#" --> Documentation: home-assistant/companion.home-assistant# ## Any other notes <!-- If there is any other information of note, like if this Pull Request is part of a bigger change, please include it here. -->
260 lines
10 KiB
Swift
260 lines
10 KiB
Swift
import Alamofire
|
|
import Foundation
|
|
import HAKit
|
|
import PromiseKit
|
|
import Shared
|
|
import SwiftUI
|
|
|
|
class OnboardingAuth {
|
|
func failureController(error: Error) -> UIViewController {
|
|
UIHostingController(rootView: OnboardingErrorView(error: error))
|
|
}
|
|
|
|
var login: OnboardingAuthLogin = OnboardingAuthLoginImpl()
|
|
var tokenExchange: OnboardingAuthTokenExchange = OnboardingAuthTokenExchangeImpl()
|
|
var preSteps: [OnboardingAuthPreStep.Type] = [
|
|
OnboardingAuthStepClientCertificate.self,
|
|
OnboardingAuthStepConnectivity.self,
|
|
]
|
|
var postSteps: [OnboardingAuthPostStep.Type] = [
|
|
OnboardingAuthStepDeviceNaming.self,
|
|
OnboardingAuthStepConfig.self,
|
|
OnboardingAuthStepSensors.self,
|
|
OnboardingAuthStepModels.self,
|
|
OnboardingAuthStepRegister.self,
|
|
OnboardingAuthStepNotify.self,
|
|
]
|
|
|
|
func authenticate(
|
|
to startInstance: DiscoveredHomeAssistant,
|
|
sender: UIViewController
|
|
) -> Promise<Server> {
|
|
firstly {
|
|
connect(to: startInstance, sender: sender)
|
|
}.then { [self] api -> Promise<Server> in
|
|
func steps(_ steps: OnboardingAuthStepPoint...) -> Promise<Void> {
|
|
var promise: Promise<Void> = .value(())
|
|
|
|
for step in steps {
|
|
promise = promise.then { [self] in
|
|
performPostSteps(checkPoint: step, api: api, sender: sender)
|
|
}
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
return firstly {
|
|
steps(.beforeRegister, .register, .afterRegister)
|
|
}.map {
|
|
// actually persists to outside-onboarding
|
|
Current.servers.add(identifier: api.server.identifier, serverInfo: api.server.info)
|
|
}.get { server in
|
|
// somewhat necessary so it points to the keychain-persisted version
|
|
api.server = server
|
|
// not super necessary but prevents making a duplicate connection during this session
|
|
Current.setCachedApi(api, for: api.server.identifier)
|
|
}.then { server in
|
|
steps(.complete).map { server }
|
|
}.recover(policy: .allErrors) { [self] error -> Promise<Server> in
|
|
when(resolved: undoConfigure(api: api)).then { _ in Promise<Server>(error: error) }
|
|
}
|
|
}
|
|
}
|
|
|
|
private func perform(checkPoint: OnboardingAuthStepPoint, checks: [OnboardingAuthStep]) -> Promise<Void> {
|
|
// Execute steps sequentially to allow ClientCertificate to complete before Connectivity
|
|
checks.reduce(Promise.value(())) { promise, check in
|
|
promise.then {
|
|
check.perform(point: checkPoint).tap { result in
|
|
Current.Log.info("\(type(of: check)): \(result)")
|
|
}.asVoid()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func performPreSteps(
|
|
checkPoint: OnboardingAuthStepPoint,
|
|
authDetails: OnboardingAuthDetails,
|
|
sender: UIViewController
|
|
) -> Promise<Void> {
|
|
Current.Log.info(checkPoint)
|
|
return perform(checkPoint: checkPoint, checks: preSteps.compactMap { checkType in
|
|
if checkType.supportedPoints.contains(checkPoint) {
|
|
return checkType.init(authDetails: authDetails, sender: sender)
|
|
} else {
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
private func performPostSteps(
|
|
checkPoint: OnboardingAuthStepPoint,
|
|
api: HomeAssistantAPI,
|
|
sender: UIViewController
|
|
) -> Promise<Void> {
|
|
Current.Log.info(checkPoint)
|
|
return perform(checkPoint: checkPoint, checks: postSteps.compactMap { checkType in
|
|
if checkType.supportedPoints.contains(checkPoint) {
|
|
return checkType.init(api: api, sender: sender)
|
|
} else {
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
private func connect(
|
|
to baseInstance: DiscoveredHomeAssistant,
|
|
sender: UIViewController
|
|
) -> Promise<HomeAssistantAPI> {
|
|
// we prefer internal URL first, if it's available
|
|
var instances = [(URL, DiscoveredHomeAssistant)]()
|
|
|
|
if let internalURL = baseInstance.internalURL {
|
|
instances.append((internalURL, baseInstance))
|
|
}
|
|
|
|
if let externalURL = baseInstance.externalURL {
|
|
instances.append((externalURL, with(baseInstance) {
|
|
$0.internalURL = nil
|
|
}))
|
|
}
|
|
|
|
var promise: Promise<HomeAssistantAPI> = .init(error: OnboardingAuthError(kind: .invalidURL))
|
|
|
|
for (idx, (url, instance)) in instances.enumerated() {
|
|
promise = promise.recover { [self] originalError -> Promise<HomeAssistantAPI> in
|
|
let authDetails = try OnboardingAuthDetails(baseURL: url)
|
|
|
|
return firstly {
|
|
performPreSteps(checkPoint: .beforeAuth, authDetails: authDetails, sender: sender)
|
|
}.then { [self] in
|
|
login.open(authDetails: authDetails, sender: sender)
|
|
}.then { [self] result -> Promise<HomeAssistantAPI> in
|
|
// The login web view may have been redirected to a different port/scheme; adopt that
|
|
// address so the stored server URL (and the token exchange) target the real server.
|
|
let adoptedInstance = Self.instance(
|
|
instance,
|
|
adoptingResolvedURL: result.resolvedURL,
|
|
attemptedURL: url
|
|
)
|
|
return configuredAPI(authDetails: authDetails, instance: adoptedInstance, code: result.code)
|
|
}.recover { newError -> Promise<HomeAssistantAPI> in
|
|
if idx == 0 {
|
|
// we're the first/internal url, so our error should break the placeholder one
|
|
throw newError
|
|
} else {
|
|
// preserve the error we got for the internal url
|
|
throw originalError
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return promise
|
|
}
|
|
|
|
/// When the login web view ends on a different URL than the one we started with, adopt that address
|
|
/// for the slot (internal/external) we attempted. Only same-host port/scheme redirects are adopted —
|
|
/// a different host is ignored so we never follow an unexpected server.
|
|
static func instance(
|
|
_ instance: DiscoveredHomeAssistant,
|
|
adoptingResolvedURL resolvedURL: URL?,
|
|
attemptedURL: URL
|
|
) -> DiscoveredHomeAssistant {
|
|
let attemptedBase = attemptedURL.serverBaseURL()
|
|
|
|
guard let adoptedBase = resolvedURL?.sameHostRedirectBaseURL(from: attemptedURL) else {
|
|
if let resolvedBase = resolvedURL?.serverBaseURL(), !resolvedBase.baseIsEqual(to: attemptedBase) {
|
|
// Different host, or an https->http downgrade we won't follow.
|
|
Current.Log.warning("Not adopting auth redirect to \(resolvedBase); keeping \(attemptedBase)")
|
|
}
|
|
return instance
|
|
}
|
|
|
|
Current.Log.info("Adopting redirected server URL \(adoptedBase) (was \(attemptedBase))")
|
|
|
|
return with(instance) {
|
|
if let url = $0.internalURL, url.serverBaseURL().baseIsEqual(to: attemptedBase) {
|
|
$0.internalURL = adoptedBase
|
|
}
|
|
if let url = $0.externalURL, url.serverBaseURL().baseIsEqual(to: attemptedBase) {
|
|
$0.externalURL = adoptedBase
|
|
}
|
|
$0.internalOrExternalURL = $0.internalURL ?? $0.externalURL ?? adoptedBase
|
|
}
|
|
}
|
|
|
|
private func configuredAPI(
|
|
authDetails: OnboardingAuthDetails,
|
|
instance: DiscoveredHomeAssistant,
|
|
code: String
|
|
) -> Promise<HomeAssistantAPI> {
|
|
Current.Log.info()
|
|
|
|
var connectionInfo = ConnectionInfo(discovered: instance, authDetails: authDetails)
|
|
|
|
return tokenExchange.tokenInfo(
|
|
code: code,
|
|
connectionInfo: &connectionInfo
|
|
).then { tokenInfo -> Promise<HomeAssistantAPI> in
|
|
Current.Log.verbose()
|
|
|
|
var serverInfo = ServerInfo(
|
|
name: ServerInfo.defaultName,
|
|
connection: connectionInfo,
|
|
token: tokenInfo,
|
|
version: instance.version ?? DiscoveredHomeAssistant.defaultVersion
|
|
)
|
|
|
|
let identifier = Identifier<Server>(rawValue: instance.uuid ?? UUID().uuidString)
|
|
let server = Server(
|
|
identifier: identifier,
|
|
getter: { serverInfo },
|
|
setter: { serverInfo = $0; return true }
|
|
)
|
|
|
|
return .value(HomeAssistantAPI(server: server))
|
|
}
|
|
}
|
|
|
|
private func undoConfigure(api: HomeAssistantAPI) -> Promise<Void> {
|
|
Current.Log.info()
|
|
return firstly {
|
|
when(resolved: api.tokenManager.revokeToken()).asVoid()
|
|
}.done {
|
|
api.connection.disconnect()
|
|
Current.servers.remove(identifier: api.server.identifier)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension ConnectionInfo {
|
|
init(discovered: DiscoveredHomeAssistant, authDetails: OnboardingAuthDetails) {
|
|
self.init(
|
|
externalURL: discovered.externalURL,
|
|
internalURL: discovered.internalURL,
|
|
cloudhookURL: nil,
|
|
remoteUIURL: nil,
|
|
webhookID: "",
|
|
webhookSecret: nil,
|
|
internalSSIDs: Current.connectivity.currentWiFiSSID().map { [$0] },
|
|
internalHardwareAddresses: nil,
|
|
isLocalPushEnabled: false,
|
|
securityExceptions: authDetails.exceptions,
|
|
connectionAccessSecurityLevel: .undefined,
|
|
clientCertificate: authDetails.clientCertificate
|
|
)
|
|
|
|
// default cloud to on
|
|
useCloud = true
|
|
|
|
// if we have internal+external, we're on the internal network doing discovery
|
|
// but we don't yet have location permission to know we're on an internal ssid
|
|
if internalSSIDs == [] || internalSSIDs == nil,
|
|
discovered.internalURL != nil, discovered.externalURL != nil {
|
|
overrideActiveURLType = .internal
|
|
}
|
|
}
|
|
}
|