Merge branch 'housekeeping/fix-oauth-token-renewal' into 'develop'

Further fix for refreshing access token

See merge request funkwhale/funkwhale-android!71
This commit is contained in:
Ryan Harg 2021-08-22 07:19:37 +00:00
commit 134a6636ea
7 changed files with 91 additions and 148 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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