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:
commit
134a6636ea
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
package audio.funkwhale.ffa.utils
|
||||
|
||||
object RefreshError : Throwable()
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue