From 73631cc9e999cddb37a2ec521a70b44781995c0b Mon Sep 17 00:00:00 2001 From: Ryan Harg Date: Sun, 22 Aug 2021 09:12:57 +0200 Subject: [PATCH] Further fix for refreshing access token --- .../ffa/repositories/HttpUpstream.kt | 43 +++---- .../java/audio/funkwhale/ffa/utils/Data.kt | 120 ------------------ .../audio/funkwhale/ffa/utils/Extensions.kt | 12 +- .../audio/funkwhale/ffa/utils/FFACache.kt | 38 ++++++ .../java/audio/funkwhale/ffa/utils/OAuth.kt | 21 +++ .../audio/funkwhale/ffa/utils/RefreshError.kt | 4 + .../java/audio/funkwhale/ffa/utils/Util.kt | 1 - 7 files changed, 91 insertions(+), 148 deletions(-) delete mode 100644 app/src/main/java/audio/funkwhale/ffa/utils/Data.kt create mode 100644 app/src/main/java/audio/funkwhale/ffa/utils/FFACache.kt create mode 100644 app/src/main/java/audio/funkwhale/ffa/utils/RefreshError.kt diff --git a/app/src/main/java/audio/funkwhale/ffa/repositories/HttpUpstream.kt b/app/src/main/java/audio/funkwhale/ffa/repositories/HttpUpstream.kt index 08b53ad..f01c4d3 100644 --- a/app/src/main/java/audio/funkwhale/ffa/repositories/HttpUpstream.kt +++ b/app/src/main/java/audio/funkwhale/ffa/repositories/HttpUpstream.kt @@ -2,12 +2,12 @@ package audio.funkwhale.ffa.repositories import android.content.Context import android.net.Uri +import android.util.Log import audio.funkwhale.ffa.utils.* import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.FuelError import com.github.kittinunf.fuel.core.ResponseDeserializable import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult -import com.github.kittinunf.fuel.coroutines.awaitObjectResult import com.github.kittinunf.result.Result import com.google.gson.Gson import kotlinx.coroutines.Dispatchers.IO @@ -33,8 +33,6 @@ class HttpUpstream>( Progressive } - private val http = HTTP(context, oAuth) - override fun fetch(size: Int): Flow> = flow> { context?.let { @@ -42,14 +40,13 @@ class HttpUpstream>( val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1 - val url = - Uri.parse(url) - .buildUpon() - .appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString()) - .appendQueryParameter("page", page.toString()) - .appendQueryParameter("scope", Settings.getScopes().joinToString(" ")) - .build() - .toString() + val url = Uri.parse(url) + .buildUpon() + .appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString()) + .appendQueryParameter("page", page.toString()) + .appendQueryParameter("scope", Settings.getScopes().joinToString(" ")) + .build() + .toString() get(it, url).fold( { response -> @@ -88,16 +85,16 @@ class HttpUpstream>( } suspend fun get(context: Context, url: String): Result { + Log.i("HttpUpstream", "get() - url: $url") return try { - val request = Fuel.get(mustNormalizeUrl(url)).apply { + val normalizedUrl = mustNormalizeUrl(url) + val request = Fuel.get(normalizedUrl).apply { authorize(context, oAuth) } val (_, response, result) = request.awaitObjectResponseResult(GenericDeserializer(type)) - if (response.statusCode == 401) { - return retryGet(url) + return retryGet(normalizedUrl) } - result } catch (e: Exception) { Result.error(FuelError.wrap(e)) @@ -105,19 +102,15 @@ class HttpUpstream>( } private suspend fun retryGet(url: String): Result { + Log.i("HttpUpstream", "retryGet() - url: $url") context?.let { return try { - return if (http.refresh()) { - val request = Fuel.get(mustNormalizeUrl(url)).apply { - if (!Settings.isAnonymous()) { - header("Authorization", "Bearer ${oAuth.state().accessToken}") - } - } - - request.awaitObjectResult(GenericDeserializer(type)) - } else { - Result.Failure(FuelError.wrap(RefreshError)) + oAuth.refreshAccessToken(context) + val request = Fuel.get(url).apply { + authorize(context, oAuth) } + val (_, _, result) = request.awaitObjectResponseResult(GenericDeserializer(type)) + result } catch (e: Exception) { Result.error(FuelError.wrap(e)) } diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/Data.kt b/app/src/main/java/audio/funkwhale/ffa/utils/Data.kt deleted file mode 100644 index 02c82d6..0000000 --- a/app/src/main/java/audio/funkwhale/ffa/utils/Data.kt +++ /dev/null @@ -1,120 +0,0 @@ -package audio.funkwhale.ffa.utils - -import android.content.Context -import audio.funkwhale.ffa.activities.FwCredentials -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.core.FuelError -import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult -import com.github.kittinunf.fuel.coroutines.awaitObjectResult -import com.github.kittinunf.fuel.gson.gsonDeserializerOf -import com.github.kittinunf.result.Result -import com.preference.PowerPreference -import java.io.BufferedReader -import java.io.File -import java.nio.charset.Charset -import java.security.MessageDigest - -object RefreshError : Throwable() - -class HTTP( - val context: Context?, - val oAuth: OAuth -) { - - suspend fun refresh(): Boolean { - context?.let { - val body = mapOf( - "username" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS) - .getString("username"), - "password" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS) - .getString("password") - ).toList() - - val result = Fuel.post(mustNormalizeUrl("/api/v1/token"), body).apply { - if (!Settings.isAnonymous()) { - authorize(it, oAuth) - header("Authorization", "Bearer ${oAuth.state().accessToken}") - } - } - .awaitObjectResult(gsonDeserializerOf(FwCredentials::class.java)) - - return result.fold( - { data -> - PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS) - .setString("access_token", data.token) - - true - }, - { false } - ) - } - throw IllegalStateException("Illegal state: context is null") - } - - suspend inline fun get(url: String): Result { - - context?.let { - val request = Fuel.get(mustNormalizeUrl(url)).apply { - if (!Settings.isAnonymous()) { - authorize(it, oAuth) - header("Authorization", "Bearer ${oAuth.state().accessToken}") - } - } - - val (_, response, result) = request.awaitObjectResponseResult(gsonDeserializerOf(T::class.java)) - - if (response.statusCode == 401) { - return retryGet(url) - } else { - return result - } - } - throw IllegalStateException("Illegal state: context is null") - } - - suspend inline fun retryGet( - url: String - ): Result { - context?.let { - val request = Fuel.get(mustNormalizeUrl(url)).apply { - if (!Settings.isAnonymous()) { - authorize(context,oAuth) - header("Authorization", "Bearer ${oAuth.state().accessToken}") - } - } - request.awaitObjectResult(gsonDeserializerOf(T::class.java)) - } - throw IllegalStateException("Illegal state: context is null") - } -} - -object FFACache { - private fun key(key: String): String { - val md = MessageDigest.getInstance("SHA-1") - val digest = md.digest(key.toByteArray(Charset.defaultCharset())) - - return digest.fold("", { acc, it -> acc + "%02x".format(it) }) - } - - fun set(context: Context?, key: String, value: ByteArray) = context?.let { - with(File(it.cacheDir, key(key))) { - writeBytes(value) - } - } - - fun get(context: Context?, key: String): BufferedReader? = context?.let { - try { - with(File(it.cacheDir, key(key))) { - bufferedReader() - } - } catch (e: Exception) { - return null - } - } - - fun delete(context: Context?, key: String) = context?.let { - with(File(it.cacheDir, key(key))) { - delete() - } - } -} diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt b/app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt index 5ac84d8..ababd6e 100644 --- a/app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt +++ b/app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt @@ -22,8 +22,11 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import net.openid.appauth.ClientSecretPost +import java.text.SimpleDateFormat +import java.util.* import kotlin.coroutines.CoroutineContext + inline fun Flow>.untilNetwork( scope: CoroutineScope, context: CoroutineContext = Main, @@ -68,9 +71,8 @@ fun Request.authorize(context: Context, oAuth: OAuth): Request { this@authorize.apply { if (!Settings.isAnonymous()) { oAuth.state().let { state -> - val now = SystemClock.currentThreadTimeMillis() state.accessTokenExpirationTime?.let { - Log.i("Request.authorize()", "Accesstoken expiration: ${it - now}") + Log.i("Request.authorize()", "Accesstoken expiration: ${Date(it).format()}") } val old = state.accessToken val auth = ClientSecretPost(oAuth.state().clientSecret) @@ -100,3 +102,9 @@ fun FuelError.formatResponseMessage(): String { fun Download.getMetadata(): DownloadInfo? = Gson().fromJson(String(this.request.data), DownloadInfo::class.java) + +val ISO_8601_DATE_TIME_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + +fun Date.format(): String { + return ISO_8601_DATE_TIME_FORMAT.format(this) +} \ No newline at end of file diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/FFACache.kt b/app/src/main/java/audio/funkwhale/ffa/utils/FFACache.kt new file mode 100644 index 0000000..446c209 --- /dev/null +++ b/app/src/main/java/audio/funkwhale/ffa/utils/FFACache.kt @@ -0,0 +1,38 @@ +package audio.funkwhale.ffa.utils + +import android.content.Context +import java.io.BufferedReader +import java.io.File +import java.nio.charset.Charset +import java.security.MessageDigest + +object FFACache { + private fun key(key: String): String { + val md = MessageDigest.getInstance("SHA-1") + val digest = md.digest(key.toByteArray(Charset.defaultCharset())) + + return digest.fold("", { acc, it -> acc + "%02x".format(it) }) + } + + fun set(context: Context?, key: String, value: ByteArray) = context?.let { + with(File(it.cacheDir, key(key))) { + writeBytes(value) + } + } + + fun get(context: Context?, key: String): BufferedReader? = context?.let { + try { + with(File(it.cacheDir, key(key))) { + bufferedReader() + } + } catch (e: Exception) { + return null + } + } + + fun delete(context: Context?, key: String) = context?.let { + with(File(it.cacheDir, key(key))) { + delete() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt b/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt index 6576dc3..b2d6505 100644 --- a/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt +++ b/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt @@ -71,6 +71,27 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory return false } + fun refreshAccessToken(context: Context): Boolean { + Log.i("OAuth", "refreshAccessToken()") + val state = tryState() + return if (state != null) { + val refreshRequest = state.createTokenRefreshRequest() + val auth = ClientSecretPost(state.clientSecret) + runBlocking { + service(context).performTokenRequest(refreshRequest, auth) { response, e -> + state.apply { + Log.i("OAuth", "applying new autState") + update(response, e) + save() + } + } + } + true + } else { + false + } + } + private fun doTryRefreshAccessToken( state: AuthState, context: Context diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/RefreshError.kt b/app/src/main/java/audio/funkwhale/ffa/utils/RefreshError.kt new file mode 100644 index 0000000..1f572a4 --- /dev/null +++ b/app/src/main/java/audio/funkwhale/ffa/utils/RefreshError.kt @@ -0,0 +1,4 @@ +package audio.funkwhale.ffa.utils + +object RefreshError : Throwable() + diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/Util.kt b/app/src/main/java/audio/funkwhale/ffa/utils/Util.kt index 1a7f62a..687b536 100644 --- a/app/src/main/java/audio/funkwhale/ffa/utils/Util.kt +++ b/app/src/main/java/audio/funkwhale/ffa/utils/Util.kt @@ -66,7 +66,6 @@ fun mustNormalizeUrl(rawUrl: String): String { val fallbackHost = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname") val uri = URI(rawUrl).takeIf { it.host != null } ?: URI("$fallbackHost$rawUrl") - return uri.toString() }