From 73631cc9e999cddb37a2ec521a70b44781995c0b Mon Sep 17 00:00:00 2001
From: Ryan Harg <ryan.harg@mailbox.org>
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 08b53ade..f01c4d38 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<D : Any, R : OtterResponse<D>>(
     Progressive
   }
 
-  private val http = HTTP(context, oAuth)
-
   override fun fetch(size: Int): Flow<Repository.Response<D>> = flow<Repository.Response<D>> {
 
     context?.let {
@@ -42,14 +40,13 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
 
       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<D : Any, R : OtterResponse<D>>(
   }
 
   suspend fun get(context: Context, url: String): Result<R, FuelError> {
+    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<R>(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<D : Any, R : OtterResponse<D>>(
   }
 
   private suspend fun retryGet(url: String): Result<R, FuelError> {
+    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<R>(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 02c82d60..00000000
--- 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 <reified T : Any> get(url: String): Result<T, FuelError> {
-
-    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 <reified T : Any> retryGet(
-    url: String
-  ): Result<T, FuelError> {
-    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 5ac84d82..ababd6e9 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 <D> Flow<Repository.Response<D>>.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 00000000..446c2092
--- /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 6576dc37..b2d65055 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 00000000..1f572a41
--- /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 1a7f62af..687b5361 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()
 }
 
-- 
GitLab