Browse Source

Further fix for refreshing access token

deploy-in-docker
Ryan Harg 3 years ago committed by Ryan Harg
parent
commit
73631cc9e9
  1. 43
      app/src/main/java/audio/funkwhale/ffa/repositories/HttpUpstream.kt
  2. 120
      app/src/main/java/audio/funkwhale/ffa/utils/Data.kt
  3. 12
      app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt
  4. 38
      app/src/main/java/audio/funkwhale/ffa/utils/FFACache.kt
  5. 21
      app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt
  6. 4
      app/src/main/java/audio/funkwhale/ffa/utils/RefreshError.kt
  7. 1
      app/src/main/java/audio/funkwhale/ffa/utils/Util.kt

43
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.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import audio.funkwhale.ffa.utils.* import audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.FuelError import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.fuel.core.ResponseDeserializable import com.github.kittinunf.fuel.core.ResponseDeserializable
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
import com.github.kittinunf.result.Result import com.github.kittinunf.result.Result
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -33,8 +33,6 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
Progressive Progressive
} }
private val http = HTTP(context, oAuth)
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow<Repository.Response<D>> { override fun fetch(size: Int): Flow<Repository.Response<D>> = flow<Repository.Response<D>> {
context?.let { context?.let {
@ -42,14 +40,13 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1 val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
val url = val url = Uri.parse(url)
Uri.parse(url) .buildUpon()
.buildUpon() .appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString())
.appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString()) .appendQueryParameter("page", page.toString())
.appendQueryParameter("page", page.toString()) .appendQueryParameter("scope", Settings.getScopes().joinToString(" "))
.appendQueryParameter("scope", Settings.getScopes().joinToString(" ")) .build()
.build() .toString()
.toString()
get(it, url).fold( get(it, url).fold(
{ response -> { response ->
@ -88,16 +85,16 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
} }
suspend fun get(context: Context, url: String): Result<R, FuelError> { suspend fun get(context: Context, url: String): Result<R, FuelError> {
Log.i("HttpUpstream", "get() - url: $url")
return try { return try {
val request = Fuel.get(mustNormalizeUrl(url)).apply { val normalizedUrl = mustNormalizeUrl(url)
val request = Fuel.get(normalizedUrl).apply {
authorize(context, oAuth) authorize(context, oAuth)
} }
val (_, response, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type)) val (_, response, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
if (response.statusCode == 401) { if (response.statusCode == 401) {
return retryGet(url) return retryGet(normalizedUrl)
} }
result result
} catch (e: Exception) { } catch (e: Exception) {
Result.error(FuelError.wrap(e)) 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> { private suspend fun retryGet(url: String): Result<R, FuelError> {
Log.i("HttpUpstream", "retryGet() - url: $url")
context?.let { context?.let {
return try { return try {
return if (http.refresh()) { oAuth.refreshAccessToken(context)
val request = Fuel.get(mustNormalizeUrl(url)).apply { val request = Fuel.get(url).apply {
if (!Settings.isAnonymous()) { authorize(context, oAuth)
header("Authorization", "Bearer ${oAuth.state().accessToken}")
}
}
request.awaitObjectResult(GenericDeserializer(type))
} else {
Result.Failure(FuelError.wrap(RefreshError))
} }
val (_, _, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
result
} catch (e: Exception) { } catch (e: Exception) {
Result.error(FuelError.wrap(e)) Result.error(FuelError.wrap(e))
} }

120
app/src/main/java/audio/funkwhale/ffa/utils/Data.kt

@ -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()
}
}
}

12
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.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.openid.appauth.ClientSecretPost import net.openid.appauth.ClientSecretPost
import java.text.SimpleDateFormat
import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
inline fun <D> Flow<Repository.Response<D>>.untilNetwork( inline fun <D> Flow<Repository.Response<D>>.untilNetwork(
scope: CoroutineScope, scope: CoroutineScope,
context: CoroutineContext = Main, context: CoroutineContext = Main,
@ -68,9 +71,8 @@ fun Request.authorize(context: Context, oAuth: OAuth): Request {
this@authorize.apply { this@authorize.apply {
if (!Settings.isAnonymous()) { if (!Settings.isAnonymous()) {
oAuth.state().let { state -> oAuth.state().let { state ->
val now = SystemClock.currentThreadTimeMillis()
state.accessTokenExpirationTime?.let { 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 old = state.accessToken
val auth = ClientSecretPost(oAuth.state().clientSecret) val auth = ClientSecretPost(oAuth.state().clientSecret)
@ -100,3 +102,9 @@ fun FuelError.formatResponseMessage(): String {
fun Download.getMetadata(): DownloadInfo? = fun Download.getMetadata(): DownloadInfo? =
Gson().fromJson(String(this.request.data), DownloadInfo::class.java) 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)
}

38
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()
}
}
}

21
app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt

@ -71,6 +71,27 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory
return false 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( private fun doTryRefreshAccessToken(
state: AuthState, state: AuthState,
context: Context context: Context

4
app/src/main/java/audio/funkwhale/ffa/utils/RefreshError.kt

@ -0,0 +1,4 @@
package audio.funkwhale.ffa.utils
object RefreshError : Throwable()

1
app/src/main/java/audio/funkwhale/ffa/utils/Util.kt

@ -66,7 +66,6 @@ fun mustNormalizeUrl(rawUrl: String): String {
val fallbackHost = val fallbackHost =
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname") PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
val uri = URI(rawUrl).takeIf { it.host != null } ?: URI("$fallbackHost$rawUrl") val uri = URI(rawUrl).takeIf { it.host != null } ?: URI("$fallbackHost$rawUrl")
return uri.toString() return uri.toString()
} }

Loading…
Cancel
Save