From 53d99e46e6b34d4437f46266cb797a65c0736803 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 27 Aug 2020 16:42:03 -0300 Subject: [cashier] don't crash on unexpected network input --- .../java/net/taler/cashier/config/ConfigManager.kt | 141 +++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt (limited to 'cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt') diff --git a/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt new file mode 100644 index 0000000..a18073d --- /dev/null +++ b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt @@ -0,0 +1,141 @@ +/* + * This file is part of GNU Taler + * (C) 2020 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.cashier.config + +import android.annotation.SuppressLint +import android.app.Application +import android.util.Log +import androidx.annotation.UiThread +import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.http.HttpHeaders.Authorization +import io.ktor.http.HttpStatusCode.Companion.Unauthorized +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.taler.cashier.Response +import net.taler.cashier.Response.Companion.response +import net.taler.common.getIncompatibleStringOrNull +import net.taler.lib.common.Version + +private val VERSION_BANK = Version(0, 0, 0) +private const val PREF_NAME = "net.taler.cashier.prefs" +private const val PREF_KEY_BANK_URL = "bankUrl" +private const val PREF_KEY_USERNAME = "username" +private const val PREF_KEY_PASSWORD = "password" +private const val PREF_KEY_CURRENCY = "currency" + +private val TAG = ConfigManager::class.java.simpleName + +class ConfigManager( + private val app: Application, + private val scope: CoroutineScope, + private val httpClient: HttpClient +) { + + val configDestination = ConfigFragmentDirections.actionGlobalConfigFragment() + + private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) + private val prefs = EncryptedSharedPreferences.create( + PREF_NAME, masterKeyAlias, app, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + internal var config = Config( + bankUrl = prefs.getString(PREF_KEY_BANK_URL, "")!!, + username = prefs.getString(PREF_KEY_USERNAME, "")!!, + password = prefs.getString(PREF_KEY_PASSWORD, "")!! + ) + + private val mCurrency = MutableLiveData( + prefs.getString(PREF_KEY_CURRENCY, null) + ) + internal val currency: LiveData = mCurrency + + private val mConfigResult = MutableLiveData() + val configResult: LiveData = mConfigResult + + fun hasConfig() = config.bankUrl.isNotEmpty() + && config.username.isNotEmpty() + && config.password.isNotEmpty() + + /** + * Start observing [configResult] after calling this to get the result async. + * Warning: Ignore null results that are used to reset old results. + */ + @UiThread + fun checkAndSaveConfig(config: Config) = scope.launch { + mConfigResult.value = null + checkConfig(config).onError { failure -> + val result = if (failure.isOffline(app)) { + ConfigResult.Offline + } else { + ConfigResult.Error(failure.statusCode == Unauthorized, failure.msg) + } + mConfigResult.postValue(result) + }.onSuccess { response -> + val versionIncompatible = + VERSION_BANK.getIncompatibleStringOrNull(app, response.version) + val result = if (versionIncompatible != null) { + ConfigResult.Error(false, versionIncompatible) + } else { + mCurrency.postValue(response.currency) + prefs.edit().putString(PREF_KEY_CURRENCY, response.currency).apply() + // save config + saveConfig(config) + ConfigResult.Success + } + mConfigResult.postValue(result) + } + } + + private suspend fun checkConfig(config: Config): Response = + withContext(Dispatchers.IO) { + val url = "${config.bankUrl}/config" + Log.d(TAG, "Checking config: $url") + response { + httpClient.get(url) { + // TODO why does that not fail already? + header(Authorization, config.basicAuth) + } as ConfigResponse + } + } + + @WorkerThread + @SuppressLint("ApplySharedPref") + internal fun saveConfig(config: Config) { + this.config = config + prefs.edit() + .putString(PREF_KEY_BANK_URL, config.bankUrl) + .putString(PREF_KEY_USERNAME, config.username) + .putString(PREF_KEY_PASSWORD, config.password) + .commit() + } + + fun lock() { + saveConfig(config.copy(password = "")) + } + +} -- cgit v1.2.3