* fix(auth): await cookie scope hydration before ticket mint and request loading (#961) * refactor(auth): scope latch activation to active store hydration --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
913ae1275d
commit
764cb3455f
3 files changed
+66
-9
No files matched your search
@@ -11,6 +11,7 @@ import okhttp3.CookieJar
|
|||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +64,9 @@ class PersistentCookieJar(
|
|||||||
*/
|
*/
|
||||||
fun getCookie(name: String): Cookie? {
|
fun getCookie(name: String): Cookie? {
|
||||||
val serverId = currentServerId.get()
|
val serverId = currentServerId.get()
|
||||||
|
if (!loadedScopes.contains(serverId)) {
|
||||||
|
loadLatches[serverId]?.await(5, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
val hosts = cache[serverId] ?: return null
|
val hosts = cache[serverId] ?: return null
|
||||||
for (bucket in hosts.values) {
|
for (bucket in hosts.values) {
|
||||||
synchronized(bucket) {
|
synchronized(bucket) {
|
||||||
@@ -126,6 +130,9 @@ class PersistentCookieJar(
|
|||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
val serverId = currentServerId.get()
|
val serverId = currentServerId.get()
|
||||||
|
if (!loadedScopes.contains(serverId)) {
|
||||||
|
loadLatches[serverId]?.await(5, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
val hostKey = url.host
|
val hostKey = url.host
|
||||||
val hosts = cache[serverId] ?: return emptyList()
|
val hosts = cache[serverId] ?: return emptyList()
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
@@ -152,16 +159,21 @@ class PersistentCookieJar(
|
|||||||
|
|
||||||
private suspend fun ensureLoaded(serverId: String) {
|
private suspend fun ensureLoaded(serverId: String) {
|
||||||
if (loadedScopes.contains(serverId)) return
|
if (loadedScopes.contains(serverId)) return
|
||||||
val persisted = store.load(serverId)
|
val latch = loadLatches.computeIfAbsent(serverId) { CountDownLatch(1) }
|
||||||
val byHost = ConcurrentHashMap<String, MutableList<Cookie>>()
|
try {
|
||||||
for (cookie in persisted) {
|
val persisted = store.load(serverId)
|
||||||
// Blank domain => host-only (e.g. legacy migrated session cookie).
|
val byHost = ConcurrentHashMap<String, MutableList<Cookie>>()
|
||||||
// Bucket it under a wildcard "*" so it is returned for every host.
|
for (cookie in persisted) {
|
||||||
val host = cookie.domain.removePrefix(".").ifBlank { WILDCARD_HOST }
|
// Blank domain => host-only (e.g. legacy migrated session cookie).
|
||||||
byHost.getOrPut(host) { mutableListOf() }.add(cookie)
|
// Bucket it under a wildcard "*" so it is returned for every host.
|
||||||
|
val host = cookie.domain.removePrefix(".").ifBlank { WILDCARD_HOST }
|
||||||
|
byHost.getOrPut(host) { mutableListOf() }.add(cookie)
|
||||||
|
}
|
||||||
|
cache[serverId] = byHost
|
||||||
|
loadedScopes.add(serverId)
|
||||||
|
} finally {
|
||||||
|
latch.countDown()
|
||||||
}
|
}
|
||||||
cache[serverId] = byHost
|
|
||||||
loadedScopes.add(serverId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persist(serverId: String) {
|
private fun persist(serverId: String) {
|
||||||
@@ -195,6 +207,7 @@ class PersistentCookieJar(
|
|||||||
val scope = currentServerId.get()
|
val scope = currentServerId.get()
|
||||||
cache.remove(scope)
|
cache.remove(scope)
|
||||||
loadedScopes.remove(scope)
|
loadedScopes.remove(scope)
|
||||||
|
loadLatches.remove(scope)
|
||||||
storeScope.launch { store.clear(scope) }
|
storeScope.launch { store.clear(scope) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +215,7 @@ class PersistentCookieJar(
|
|||||||
fun clearAll() {
|
fun clearAll() {
|
||||||
cache.clear()
|
cache.clear()
|
||||||
loadedScopes.clear()
|
loadedScopes.clear()
|
||||||
|
loadLatches.clear()
|
||||||
storeScope.launch { store.clearAll() }
|
storeScope.launch { store.clearAll() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.util.Log
|
|||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.m57.hermescontrol.BuildConfig
|
import com.m57.hermescontrol.BuildConfig
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
|
import com.m57.hermescontrol.data.remote.CookieManager
|
||||||
import com.m57.hermescontrol.data.remote.DashboardSessionTokenRefresher
|
import com.m57.hermescontrol.data.remote.DashboardSessionTokenRefresher
|
||||||
import com.m57.hermescontrol.data.remote.NetworkMonitor
|
import com.m57.hermescontrol.data.remote.NetworkMonitor
|
||||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||||
@@ -493,6 +494,9 @@ object HermesWsClient {
|
|||||||
// read throws NetworkOnMainThreadException and the mint fails.
|
// read throws NetworkOnMainThreadException and the mint fails.
|
||||||
val (code, body) =
|
val (code, body) =
|
||||||
kotlinx.coroutines.runBlocking(Dispatchers.IO) {
|
kotlinx.coroutines.runBlocking(Dispatchers.IO) {
|
||||||
|
if (CookieManager.isInitialized()) {
|
||||||
|
CookieManager.useStore(CookieManager.cookieJar.currentServer())
|
||||||
|
}
|
||||||
client.newCall(request).execute().use { resp ->
|
client.newCall(request).execute().use { resp ->
|
||||||
resp.code to resp.body.string()
|
resp.code to resp.body.string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.m57.hermescontrol.data.remote
|
package com.m57.hermescontrol.data.remote
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
@@ -239,4 +243,39 @@ class PersistentCookieJarTest {
|
|||||||
|
|
||||||
assertEquals("secure-value", jar.getSessionCookieValue())
|
assertEquals("secure-value", jar.getSessionCookieValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadForRequest_awaitsAsyncScopeHydration() =
|
||||||
|
runTest {
|
||||||
|
val fakeStore = FakeEncryptedCookieStore()
|
||||||
|
fakeStore.save("cold-server", listOf(sessionCookie("dash.local", "cold-cookie-xyz")))
|
||||||
|
|
||||||
|
val store =
|
||||||
|
object : CookieStore by fakeStore {
|
||||||
|
override suspend fun load(serverId: String): List<Cookie> {
|
||||||
|
delay(50)
|
||||||
|
return fakeStore.load(serverId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val testScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
val jar =
|
||||||
|
PersistentCookieJar(
|
||||||
|
store = store,
|
||||||
|
storeScope = testScope,
|
||||||
|
initialServerId = "cold-server",
|
||||||
|
)
|
||||||
|
|
||||||
|
val job =
|
||||||
|
testScope.launch {
|
||||||
|
jar.useStore("cold-server")
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(10)
|
||||||
|
|
||||||
|
val loaded = jar.loadForRequest("http://dash.local/api/status".toHttpUrl())
|
||||||
|
assertEquals(1, loaded.size)
|
||||||
|
assertEquals("cold-cookie-xyz", loaded[0].value)
|
||||||
|
job.join()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user