From 5f540a5b45dd1fe32354ece35c2c00a3bafe6aa0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 6 Dec 2019 23:46:32 +0100 Subject: [PATCH 1/2] Support entering a RiotWeb client URL instead of the homeserver URL during connection (#744) --- CHANGES.md | 2 +- .../android/api/auth/data/LoginFlowResult.kt | 3 +- .../matrix/android/api/failure/Failure.kt | 2 +- .../matrix/android/internal/auth/AuthAPI.kt | 7 ++ .../auth/DefaultAuthenticationService.kt | 74 +++++++++++++++++-- .../android/internal/auth/data/RiotConfig.kt | 28 +++++++ .../internal/network/RetrofitExtensions.kt | 4 + .../vector/riotx/core/error/ErrorFormatter.kt | 10 +++ .../riotx/features/login/LoginViewModel.kt | 2 +- vector/src/main/res/values/strings.xml | 1 + 10 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/RiotConfig.kt diff --git a/CHANGES.md b/CHANGES.md index d6e107299f..4e59c0484f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features ✨: - Improvements 🙌: - - + - Support entering a RiotWeb client URL instead of the homeserver URL during connection (#744) Other changes: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt index f0d0c61d58..dd0c93a41c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt @@ -22,7 +22,8 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse sealed class LoginFlowResult { data class Success( val loginFlowResponse: LoginFlowResponse, - val isLoginAndRegistrationSupported: Boolean + val isLoginAndRegistrationSupported: Boolean, + val homeServerUrl: String ) : LoginFlowResult() object OutdatedHomeserver : LoginFlowResult() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt index 9d42e8388c..4d44e3346b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt @@ -36,7 +36,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) // When server send an error, but it cannot be interpreted as a MatrixError - data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody)) + data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody")) data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString())) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index a1c746a299..306a3846bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed import im.vector.matrix.android.internal.auth.registration.* import im.vector.matrix.android.internal.network.NetworkConstants @@ -31,6 +32,12 @@ import retrofit2.http.* */ internal interface AuthAPI { + /** + * Get a Riot config file + */ + @GET("config.json") + fun getRiotConfig(): Call + /** * Get the version information of the homeserver */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index e7cf999820..b10ed7217f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -16,16 +16,19 @@ package im.vector.matrix.android.internal.auth +import android.net.Uri import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.* import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.data.LoginFlowResponse +import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard @@ -40,6 +43,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated private val okHttpClient: Lazy, @@ -84,7 +88,12 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated { if (it is LoginFlowResult.Success) { // The homeserver exists and up to date, keep the config - pendingSessionData = PendingSessionData(homeServerConnectionConfig) + // Homeserver url may have been changed, if it was a Riot url + val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(it.homeServerUrl) + ) + + pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig) .also { data -> pendingSessionStore.savePendingSessionData(data) } } callback.onSuccess(it) @@ -97,20 +106,71 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated .toCancelable() } - private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) { + private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + return withContext(coroutineDispatchers.io) { + val authAPI = buildAuthAPI(homeServerConnectionConfig) + + // First check the homeserver version + runCatching { + executeRequest { + apiCall = authAPI.versions() + } + } + .map { versions -> + // Ok, it seems that the homeserver url is valid + getLoginFlowResult(authAPI, versions, homeServerConnectionConfig.homeServerUri.toString()) + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // It's maybe a Riot url? + getRiotLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + } + + private suspend fun getRiotLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { val authAPI = buildAuthAPI(homeServerConnectionConfig) - // First check the homeserver version - val versions = executeRequest { - apiCall = authAPI.versions() + // Ok, try to get the config.json file of a RiotWeb client + val riotConfig = executeRequest { + apiCall = authAPI.getRiotConfig() } - if (versions.isSupportedBySdk()) { + if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { + // Ok, good sign, we got a default hs url + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest { + apiCall = newAuthAPI.versions() + } + + return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) + } else { + // Config exists, but we cannot retrieve a default homeserver url + throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + } + } + + private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { + return if (versions.isSupportedBySdk()) { // Get the login flow val loginFlowResponse = executeRequest { apiCall = authAPI.getLoginFlows() } - LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk()) + LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) } else { // Not supported LoginFlowResult.OutdatedHomeserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/RiotConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/RiotConfig.kt new file mode 100644 index 0000000000..aebcfe0305 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/RiotConfig.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RiotConfig( + // There are plenty of other elements in the file config.json of a RiotWeb client, but for the moment only one is interesting + // Ex: "brand", "branding", etc. + @Json(name = "default_hs_url") + val defaultHomeServerUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index 29b20f9739..fa0b9a1f1c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network import com.squareup.moshi.JsonDataException +import com.squareup.moshi.JsonEncodingException import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError @@ -106,6 +107,9 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure { } catch (ex: JsonDataException) { // This is not a MatrixError Timber.w("The error returned by the server is not a MatrixError") + } catch (ex: JsonEncodingException) { + // This is not a MatrixError, HTML code? + Timber.w("The error returned by the server is not a MatrixError, probably HTML string") } return Failure.OtherServerError(errorBodyStr, httpCode) diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 621031f166..6f34415d9a 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider +import java.net.HttpURLConnection import java.net.SocketTimeoutException import java.net.UnknownHostException import javax.inject.Inject @@ -76,6 +77,15 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi } } } + is Failure.OtherServerError -> { + when (throwable.httpCode) { + HttpURLConnection.HTTP_NOT_FOUND -> + // homeserver not found + stringProvider.getString(R.string.login_error_no_homeserver_found) + else -> + throwable.localizedMessage + } + } else -> throwable.localizedMessage } ?: stringProvider.getString(R.string.unknown_error) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index de76f6b416..00207cbfbf 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -539,7 +539,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi setState { copy( asyncHomeServerLoginFlowRequest = Uninitialized, - homeServerUrl = action.homeServerUrl, + homeServerUrl = data.homeServerUrl, loginMode = loginMode, loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList() ) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 57e9ca35d6..2e4d04354b 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -284,6 +284,7 @@ Unable to register : email ownership failure Please enter a valid URL This URL is not reachable, please check it + This is not a valid Matrix server address Cannot reach a homeserver at this URL, please check it Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect Mobile From 94afd3e66da4125a540546a5035ad1f9a5663f66 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 7 Dec 2019 11:05:18 +0100 Subject: [PATCH 2/2] Add example of config without default homeserver url --- .../android/internal/auth/DefaultAuthenticationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index b10ed7217f..93349f4bbc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -159,7 +159,7 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) } else { - // Config exists, but we cannot retrieve a default homeserver url + // Config exists, but there is no default homeserver url (ex: https://riot.im/app) throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) } }