Loading cardinal-android/app/src/main/java/earth/maps/cardinal/data/sync/MurenaAccountDataSource.kt +34 −2 Original line number Diff line number Diff line Loading @@ -43,7 +43,8 @@ import javax.inject.Singleton data class MurenaWorkspaceConfig( val webDavUrl: String, val username: String, val credentials: MurenaAuthToken val credentials: MurenaAuthToken, val expectedHost: String? = null ) object MurenaWorkspaceAccount { Loading @@ -55,6 +56,7 @@ object MurenaWorkspaceAccount { const val KEY_USER_ID = "oc_id" const val KEY_USED_QUOTA = "used_quota" const val KEY_TOTAL_QUOTA = "total_quota" const val KEY_EXPECTED_HOST = "expected_host" const val AUTH_TOKEN_TYPE = "oauth2-access-token" private const val DEFAULT_WEBDAV_FILES_PREFIX = "/remote.php/dav/files/" Loading Loading @@ -203,6 +205,7 @@ interface MurenaAccountStore { fun getAccount(account: MurenaAccount): Account? fun getEmail(account: Account): String? fun getUserData(account: Account, key: String): String? fun setUserData(account: Account, key: String, value: String) } @Singleton Loading Loading @@ -254,6 +257,14 @@ class AndroidMurenaAccountStore @Inject constructor( null } } override fun setUserData(account: Account, key: String, value: String) { try { accountManager.setUserData(account, key, value) } catch (exception: SecurityException) { Log.w(TAG, "Missing permission to write userData($key)", exception) } } } interface MurenaCredentialProvider { Loading Loading @@ -432,6 +443,26 @@ class DefaultMurenaWorkspaceConfigFactory @Inject constructor( } } val currentHost = Uri.parse(baseUrl).host if (currentHost != null) { val expectedHost = accountStore.getUserData( account, MurenaWorkspaceAccount.KEY_EXPECTED_HOST ) if (expectedHost == null) { accountStore.setUserData( account, MurenaWorkspaceAccount.KEY_EXPECTED_HOST, currentHost ) } else if (!currentHost.equals(expectedHost, ignoreCase = true)) { return Result.Error(MurenaSyncError.UNTRUSTED_HOST).also { Log.w( TAG, "Murena Workspace host changed from $expectedHost to $currentHost " + "for ${account.name.redactedAccountName()}" ) } } } val userId = accountStore.getUserData(account, MurenaWorkspaceAccount.KEY_USER_ID) ?: accountStore.getUserData(account, MurenaWorkspaceAccount.KEY_USERNAME) ?: fallbackWorkspaceUserId(account).also { Loading @@ -452,7 +483,8 @@ class DefaultMurenaWorkspaceConfigFactory @Inject constructor( MurenaWorkspaceConfig( webDavUrl = MurenaWorkspaceAccount.buildWebDavUrl(baseUrl, userId), username = userId, credentials = credentials credentials = credentials, expectedHost = currentHost ) ) } Loading cardinal-android/app/src/main/java/earth/maps/cardinal/data/sync/MurenaSyncWorker.kt +1 −0 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ class MurenaSyncWorker( MurenaSyncError.ACCOUNT_NOT_FOUND, MurenaSyncError.NO_BASE_URL, MurenaSyncError.INSECURE_BASE_URL, MurenaSyncError.UNTRUSTED_HOST, MurenaSyncError.AUTH_TOKEN_UNAVAILABLE, MurenaSyncError.UNAUTHORIZED, MurenaSyncError.TLS_ERROR, Loading cardinal-android/app/src/main/java/earth/maps/cardinal/data/sync/OkHttpMurenaDriveDataSource.kt +18 −3 Original line number Diff line number Diff line Loading @@ -147,12 +147,27 @@ class OkHttpMurenaDriveDataSource @Inject constructor( private fun MurenaWorkspaceConfig.validateSecureWebDavUrl(): Result.Error<MurenaSyncError>? { val parsedUrl = webDavUrl.toHttpUrlOrNull() if (parsedUrl?.isHttps == true) return null ?: return Result.Error(MurenaSyncError.INSECURE_BASE_URL).also { Log.w(TAG, "Blocked Murena sync request to unparseable WebDAV URL") } if (!parsedUrl.isHttps) { Log.w(TAG, "Blocked Murena sync request to insecure WebDAV URL") return Result.Error(MurenaSyncError.INSECURE_BASE_URL) } if (expectedHost != null && !parsedUrl.host.equals(expectedHost, ignoreCase = true)) { Log.w( TAG, "Blocked Murena sync request to unexpected host=${parsedUrl.host}" + " expected=$expectedHost" ) return Result.Error(MurenaSyncError.UNTRUSTED_HOST) } return null } private fun Request.Builder.applyWriteCondition( writeCondition: RemoteFavoritesWriteCondition ): Request.Builder { Loading cardinal-android/app/src/main/java/earth/maps/cardinal/domain/sync/MurenaSyncError.kt +1 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ enum class MurenaSyncError : Error { ACCOUNT_NOT_FOUND, NO_BASE_URL, INSECURE_BASE_URL, UNTRUSTED_HOST, AUTH_TOKEN_UNAVAILABLE, UNAUTHORIZED, NO_INTERNET, Loading cardinal-android/app/src/main/res/values/strings.xml +1 −0 Original line number Diff line number Diff line Loading @@ -332,6 +332,7 @@ <string name="murena_sync_tls_error">Murena Workspace connection has a TLS or certificate error.</string> <string name="murena_sync_timeout">Murena Workspace request timed out.</string> <string name="murena_sync_connectivity">Could not reach Murena Workspace.</string> <string name="murena_sync_host_untrusted">Murena Workspace server host changed. Re-add your account to trust the new server.</string> <string name="murena_sync_unknown_error">Favorites sync failed.</string> <string name="murena_sync_complete">Favorites sync complete.</string> </resources> Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/data/sync/MurenaAccountDataSource.kt +34 −2 Original line number Diff line number Diff line Loading @@ -43,7 +43,8 @@ import javax.inject.Singleton data class MurenaWorkspaceConfig( val webDavUrl: String, val username: String, val credentials: MurenaAuthToken val credentials: MurenaAuthToken, val expectedHost: String? = null ) object MurenaWorkspaceAccount { Loading @@ -55,6 +56,7 @@ object MurenaWorkspaceAccount { const val KEY_USER_ID = "oc_id" const val KEY_USED_QUOTA = "used_quota" const val KEY_TOTAL_QUOTA = "total_quota" const val KEY_EXPECTED_HOST = "expected_host" const val AUTH_TOKEN_TYPE = "oauth2-access-token" private const val DEFAULT_WEBDAV_FILES_PREFIX = "/remote.php/dav/files/" Loading Loading @@ -203,6 +205,7 @@ interface MurenaAccountStore { fun getAccount(account: MurenaAccount): Account? fun getEmail(account: Account): String? fun getUserData(account: Account, key: String): String? fun setUserData(account: Account, key: String, value: String) } @Singleton Loading Loading @@ -254,6 +257,14 @@ class AndroidMurenaAccountStore @Inject constructor( null } } override fun setUserData(account: Account, key: String, value: String) { try { accountManager.setUserData(account, key, value) } catch (exception: SecurityException) { Log.w(TAG, "Missing permission to write userData($key)", exception) } } } interface MurenaCredentialProvider { Loading Loading @@ -432,6 +443,26 @@ class DefaultMurenaWorkspaceConfigFactory @Inject constructor( } } val currentHost = Uri.parse(baseUrl).host if (currentHost != null) { val expectedHost = accountStore.getUserData( account, MurenaWorkspaceAccount.KEY_EXPECTED_HOST ) if (expectedHost == null) { accountStore.setUserData( account, MurenaWorkspaceAccount.KEY_EXPECTED_HOST, currentHost ) } else if (!currentHost.equals(expectedHost, ignoreCase = true)) { return Result.Error(MurenaSyncError.UNTRUSTED_HOST).also { Log.w( TAG, "Murena Workspace host changed from $expectedHost to $currentHost " + "for ${account.name.redactedAccountName()}" ) } } } val userId = accountStore.getUserData(account, MurenaWorkspaceAccount.KEY_USER_ID) ?: accountStore.getUserData(account, MurenaWorkspaceAccount.KEY_USERNAME) ?: fallbackWorkspaceUserId(account).also { Loading @@ -452,7 +483,8 @@ class DefaultMurenaWorkspaceConfigFactory @Inject constructor( MurenaWorkspaceConfig( webDavUrl = MurenaWorkspaceAccount.buildWebDavUrl(baseUrl, userId), username = userId, credentials = credentials credentials = credentials, expectedHost = currentHost ) ) } Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/data/sync/MurenaSyncWorker.kt +1 −0 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ class MurenaSyncWorker( MurenaSyncError.ACCOUNT_NOT_FOUND, MurenaSyncError.NO_BASE_URL, MurenaSyncError.INSECURE_BASE_URL, MurenaSyncError.UNTRUSTED_HOST, MurenaSyncError.AUTH_TOKEN_UNAVAILABLE, MurenaSyncError.UNAUTHORIZED, MurenaSyncError.TLS_ERROR, Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/data/sync/OkHttpMurenaDriveDataSource.kt +18 −3 Original line number Diff line number Diff line Loading @@ -147,12 +147,27 @@ class OkHttpMurenaDriveDataSource @Inject constructor( private fun MurenaWorkspaceConfig.validateSecureWebDavUrl(): Result.Error<MurenaSyncError>? { val parsedUrl = webDavUrl.toHttpUrlOrNull() if (parsedUrl?.isHttps == true) return null ?: return Result.Error(MurenaSyncError.INSECURE_BASE_URL).also { Log.w(TAG, "Blocked Murena sync request to unparseable WebDAV URL") } if (!parsedUrl.isHttps) { Log.w(TAG, "Blocked Murena sync request to insecure WebDAV URL") return Result.Error(MurenaSyncError.INSECURE_BASE_URL) } if (expectedHost != null && !parsedUrl.host.equals(expectedHost, ignoreCase = true)) { Log.w( TAG, "Blocked Murena sync request to unexpected host=${parsedUrl.host}" + " expected=$expectedHost" ) return Result.Error(MurenaSyncError.UNTRUSTED_HOST) } return null } private fun Request.Builder.applyWriteCondition( writeCondition: RemoteFavoritesWriteCondition ): Request.Builder { Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/domain/sync/MurenaSyncError.kt +1 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ enum class MurenaSyncError : Error { ACCOUNT_NOT_FOUND, NO_BASE_URL, INSECURE_BASE_URL, UNTRUSTED_HOST, AUTH_TOKEN_UNAVAILABLE, UNAUTHORIZED, NO_INTERNET, Loading
cardinal-android/app/src/main/res/values/strings.xml +1 −0 Original line number Diff line number Diff line Loading @@ -332,6 +332,7 @@ <string name="murena_sync_tls_error">Murena Workspace connection has a TLS or certificate error.</string> <string name="murena_sync_timeout">Murena Workspace request timed out.</string> <string name="murena_sync_connectivity">Could not reach Murena Workspace.</string> <string name="murena_sync_host_untrusted">Murena Workspace server host changed. Re-add your account to trust the new server.</string> <string name="murena_sync_unknown_error">Favorites sync failed.</string> <string name="murena_sync_complete">Favorites sync complete.</string> </resources>