From 257071478e7b1e8ee22b301e9100c060bf06614f Mon Sep 17 00:00:00 2001 From: Lorinc Ambrozy Date: Wed, 17 May 2023 17:42:47 +0200 Subject: [PATCH] Use additional http headers to bypass Cloudflare SSO. (Or other SSO) --- .../settings/server/ServerSettingsFragment.kt | 7 ++ .../server/ServerSettingsPresenterImpl.kt | 11 +++ .../settings/server/ServerSettingsView.kt | 1 + app/src/main/res/xml/preferences_server.xml | 30 ++++++ .../data/integration/IntegrationRepository.kt | 16 ++++ .../impl/IntegrationRepositoryImpl.kt | 96 ++++++++++++++++--- .../integration/impl/IntegrationService.kt | 11 ++- common/src/main/res/values/strings.xml | 6 ++ 8 files changed, 163 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt index a730bdc380b..15ec0a5641c 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt @@ -233,6 +233,13 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() { } } + override fun disableAdditionalHttpHeaders(useCloud: Boolean) { + findPreference("http_headers")?.let { + it.isEnabled = !useCloud + it.isVisible = !useCloud + } + } + override fun updateExternalUrl(url: String, useCloud: Boolean) { findPreference("connection_external")?.let { it.summary = diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsPresenterImpl.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsPresenterImpl.kt index 17da4070032..7a38ebf68a7 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsPresenterImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsPresenterImpl.kt @@ -56,6 +56,10 @@ class ServerSettingsPresenterImpl @Inject constructor( "registration_name" -> serverManager.getServer(serverId)?.deviceName "connection_internal" -> (serverManager.getServer(serverId)?.connection?.getUrl(isInternal = true, force = true) ?: "").toString() "session_timeout" -> serverManager.integrationRepository(serverId).getSessionTimeOut().toString() + "header_name_1" -> serverManager.integrationRepository(serverId).getHeaderName1() + "header_name_2" -> serverManager.integrationRepository(serverId).getHeaderName2() + "header_value_1" -> serverManager.integrationRepository(serverId).getHeaderValue1() + "header_value_2" -> serverManager.integrationRepository(serverId).getHeaderValue2() else -> throw IllegalArgumentException("No string found by this key: $key") } } @@ -100,6 +104,10 @@ class ServerSettingsPresenterImpl @Inject constructor( Log.e(TAG, "Issue saving session timeout value", e) } } + "header_name_1" -> serverManager.integrationRepository(serverId).saveHeaderName1(value?.ifBlank { null }) + "header_name_2" -> serverManager.integrationRepository(serverId).saveHeaderName2(value?.ifBlank { null }) + "header_value_1" -> serverManager.integrationRepository(serverId).saveHeaderValue1(value?.ifBlank { null }) + "header_value_2" -> serverManager.integrationRepository(serverId).saveHeaderValue2(value?.ifBlank { null }) else -> throw IllegalArgumentException("No string found by this key: $key") } } @@ -142,6 +150,9 @@ class ServerSettingsPresenterImpl @Inject constructor( it.connection.getUrl(false)?.toString() ?: "", it.connection.useCloud && it.connection.canUseCloud() ) + view.disableAdditionalHttpHeaders( + it.connection.useCloud && it.connection.canUseCloud() + ) } } mainScope.launch { diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsView.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsView.kt index d95407f5ebe..65b9426cb25 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsView.kt @@ -3,6 +3,7 @@ package io.homeassistant.companion.android.settings.server interface ServerSettingsView { fun updateServerName(name: String) fun enableInternalConnection(isEnabled: Boolean) + fun disableAdditionalHttpHeaders(useCloud: Boolean) fun updateExternalUrl(url: String, useCloud: Boolean) fun updateSsids(ssids: List) fun onRemovedServer(success: Boolean, hasAnyRemaining: Boolean) diff --git a/app/src/main/res/xml/preferences_server.xml b/app/src/main/res/xml/preferences_server.xml index 15e85dcb7db..615a7f6cef2 100644 --- a/app/src/main/res/xml/preferences_server.xml +++ b/app/src/main/res/xml/preferences_server.xml @@ -64,6 +64,36 @@ app:isPreferenceVisible="false" app:useSimpleSummaryProvider="true"/> + + + + + + + >? = null for (it in server.connection.getApiUrls()) { try { - zones = integrationService.getZones(it.toHttpUrlOrNull()!!, getZonesRequest) + zones = integrationService.getZones(it.toHttpUrlOrNull()!!, getAdditionalHeaders(), getZonesRequest) } catch (e: Exception) { if (causeException == null) causeException = e // Ignore failure until we are out of URLS to try, but use the first exception as cause exception @@ -402,6 +416,38 @@ class IntegrationRepositoryImpl @AssistedInject constructor( override suspend fun setTrusted(trusted: Boolean) = localStorage.putBoolean("${serverId}_$PREF_TRUSTED", trusted) + override suspend fun getHeaderName1(): String? { + return localStorage.getString("${serverId}_$PREF_HEADER_NAME_1") + } + + override suspend fun getHeaderName2(): String? { + return localStorage.getString("${serverId}_$PREF_HEADER_NAME_2") + } + + override suspend fun getHeaderValue1(): String? { + return localStorage.getString("${serverId}_$PREF_HEADER_VALUE_1") + } + + override suspend fun getHeaderValue2(): String? { + return localStorage.getString("${serverId}_$PREF_HEADER_VALUE_2") + } + + override suspend fun saveHeaderName1(headerName1: String?) { + localStorage.putString("${serverId}_$PREF_HEADER_NAME_1", headerName1) + } + + override suspend fun saveHeaderName2(headerName2: String?) { + localStorage.putString("${serverId}_$PREF_HEADER_NAME_2", headerName2) + } + + override suspend fun saveHeaderValue1(headerValue1: String?) { + localStorage.putString("${serverId}_$PREF_HEADER_VALUE_1", headerValue1) + } + + override suspend fun saveHeaderValue2(headerValue2: String?) { + localStorage.putString("${serverId}_$PREF_HEADER_VALUE_2", headerValue2) + } + override suspend fun getNotificationRateLimits(): RateLimitResponse { val pushToken = localStorage.getString(PREF_PUSH_TOKEN) ?: "" val requestBody = RateLimitRequest(pushToken) @@ -472,7 +518,7 @@ class IntegrationRepositoryImpl @AssistedInject constructor( for (it in server.connection.getApiUrls()) { try { - response = integrationService.getConfig(it.toHttpUrlOrNull()!!, getConfigRequest) + response = integrationService.getConfig(it.toHttpUrlOrNull()!!, getAdditionalHeaders(), getConfigRequest) } catch (e: Exception) { if (causeException == null) causeException = e // Ignore failure until we are out of URLS to try, but use the first exception as cause exception @@ -556,7 +602,8 @@ class IntegrationRepositoryImpl @AssistedInject constructor( val response = integrationService.getState( url.newBuilder().addPathSegments("api/states/$entityId").build(), - serverManager.authenticationRepository(serverId).buildBearerToken() + serverManager.authenticationRepository(serverId).buildBearerToken(), + getAdditionalHeaders() ) return Entity( response.entityId, @@ -649,10 +696,12 @@ class IntegrationRepositoryImpl @AssistedInject constructor( var causeException: Exception? = null for (it in server.connection.getApiUrls()) { try { - integrationService.callWebhook(it.toHttpUrlOrNull()!!, integrationRequest).let { - // If we created sensor or it already exists - if (it.isSuccessful || it.code() == 409) { - return + integrationService + .callWebhook(it.toHttpUrlOrNull()!!, getAdditionalHeaders(), integrationRequest) + .let { + // If we created sensor or it already exists + if (it.isSuccessful || it.code() == 409) { + return } } } catch (e: Exception) { @@ -684,11 +733,13 @@ class IntegrationRepositoryImpl @AssistedInject constructor( var causeException: Exception? = null for (it in server.connection.getApiUrls()) { try { - integrationService.updateSensors(it.toHttpUrlOrNull()!!, integrationRequest).let { - it.forEach { (_, response) -> - if (response["success"] == false) { - return false - } + integrationService + .updateSensors(it.toHttpUrlOrNull()!!, getAdditionalHeaders(), integrationRequest) + .let { + it.forEach { (_, response) -> + if (response["success"] == false) { + return false + } } return true } @@ -716,6 +767,25 @@ class IntegrationRepositoryImpl @AssistedInject constructor( } } + private suspend fun getAdditionalHeaders(): Map { + + val headers = mutableMapOf() + val headerName1: String? = localStorage.getString("${serverId}_$PREF_HEADER_NAME_1") + val headerName2: String? = localStorage.getString("${serverId}_$PREF_HEADER_NAME_2") + val headerValue1: String? = localStorage.getString("${serverId}_$PREF_HEADER_VALUE_1") + val headerValue2: String? = localStorage.getString("${serverId}_$PREF_HEADER_VALUE_2") + + if (!headerName1.isNullOrEmpty() && !headerValue1.isNullOrEmpty()) { + headers[headerName1] = headerValue1 + } + + if (!headerName2.isNullOrEmpty() && !headerValue2.isNullOrEmpty()) { + headers[headerName2] = headerValue2 + } + + return headers + } + private suspend fun createUpdateRegistrationRequest(deviceRegistration: DeviceRegistration): RegisterDeviceRequest { val oldDeviceRegistration = getRegistration() val pushToken = deviceRegistration.pushToken ?: oldDeviceRegistration.pushToken diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationService.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationService.kt index 621449175a9..19fdc52ed94 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationService.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationService.kt @@ -14,45 +14,51 @@ import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.HeaderMap import retrofit2.http.POST import retrofit2.http.Url interface IntegrationService { - @POST suspend fun registerDevice( @Url url: HttpUrl, @Header("Authorization") auth: String, + @HeaderMap headers : Map, @Body request: RegisterDeviceRequest ): RegisterDeviceResponse @GET suspend fun getState( @Url url: HttpUrl, - @Header("Authorization") auth: String + @Header("Authorization") auth: String, + @HeaderMap headers : Map ): EntityResponse> @POST suspend fun callWebhook( @Url url: HttpUrl, + @HeaderMap headers : Map, @Body request: IntegrationRequest ): Response @POST suspend fun getTemplate( @Url url: HttpUrl, + @HeaderMap headers : Map, @Body request: IntegrationRequest ): Map @POST suspend fun getZones( @Url url: HttpUrl, + @HeaderMap headers : Map, @Body request: IntegrationRequest ): Array> @POST suspend fun getConfig( @Url url: HttpUrl, + @HeaderMap headers : Map, @Body request: IntegrationRequest ): GetConfigResponse @@ -65,6 +71,7 @@ interface IntegrationService { @POST suspend fun updateSensors( @Url url: HttpUrl, + @HeaderMap headers : Map, @Body request: IntegrationRequest ): Map> } diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 685d44ea97d..dce409d3963 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -256,6 +256,10 @@ landscape Get Help Grant Permission + Name of the 1. header + Name of the 2. header + Value of the 1. header + Value of the 2. header Help Hide High accuracy location @@ -263,6 +267,8 @@ History Tap and hold to reorder Unable to find your\nHome Assistant instance + Additional Http Headers + Enter Http header items with names and values. These header items will be added to all API calls to the Home Assistant server. These header items can be used (for example) to bypass Cloudflare SSO. Icon Input Booleans Input Buttons