fix: harden captcha and long poll parsing
This commit is contained in:
+35
-66
@@ -1,90 +1,62 @@
|
||||
package dev.meloda.fast.network.interceptor
|
||||
|
||||
import android.util.Log
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.datastore.CaptchaTokenResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.Executors
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class Error14HandlingInterceptor(
|
||||
// private val domains: Set<String> = emptySet(),
|
||||
) : Interceptor {
|
||||
class Error14HandlingInterceptor : Interceptor {
|
||||
|
||||
private val cookie = AtomicReference<String?>(null)
|
||||
|
||||
private companion object {
|
||||
private const val CAPTCHA_ERROR_CODE = 14
|
||||
private const val CAPTCHA_ERROR_KIND = "need_captcha"
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
private val captchaMutex = Mutex()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request().withCookie()
|
||||
val response = chain.proceed(request)
|
||||
response.parseCookie()
|
||||
|
||||
if (request.shouldSkipCaptcha()) return response
|
||||
|
||||
val redirectUri = response.getRedirectUri() ?: return response
|
||||
val token = passCaptchaAndGetToken(redirectUri)
|
||||
val token = awaitCaptchaToken(redirectUri)
|
||||
|
||||
return chain.proceed(chain.request().withCookie().withSuccessToken(token))
|
||||
}
|
||||
|
||||
private fun passCaptchaAndGetToken(redirectUri: String): String = synchronized(this) {
|
||||
val tokenResult = AtomicReference<Result<String>>(Result.failure(Exception("No result")))
|
||||
|
||||
executor.submit {
|
||||
private fun awaitCaptchaToken(redirectUri: String): String = runBlocking(Dispatchers.IO) {
|
||||
captchaMutex.withLock {
|
||||
AppSettings.setCaptchaResult(CaptchaTokenResult.Initial)
|
||||
AppSettings.setCaptchaRedirectUri(redirectUri)
|
||||
Log.d("Error14Interceptor", "passCaptchaAndGetToken: $redirectUri")
|
||||
|
||||
var job: Job? = null
|
||||
job = AppSettings.getCaptchaResultFlow()
|
||||
.listenValue(CoroutineScope(Dispatchers.IO)) {
|
||||
Log.d("Error14Interceptor", "passCaptchaAndGetToken: $it")
|
||||
if (it != CaptchaTokenResult.Initial) {
|
||||
synchronized(tokenResult) {
|
||||
Log.d(
|
||||
"Error14Interceptor",
|
||||
"passCaptchaAndGetToken: SYNCHRONIZED: $it"
|
||||
)
|
||||
tokenResult.set(wrapResult(it))
|
||||
tokenResult.notifyAll()
|
||||
job?.cancel()
|
||||
Log.d(
|
||||
"Error14Interceptor",
|
||||
"passCaptchaAndGetToken: NULL RESULT"
|
||||
)
|
||||
AppSettings.setCaptchaResult(CaptchaTokenResult.Initial)
|
||||
AppSettings.setCaptchaRedirectUri(null)
|
||||
}
|
||||
}
|
||||
try {
|
||||
withTimeout(CAPTCHA_TIMEOUT) {
|
||||
AppSettings.getCaptchaResultFlow()
|
||||
.first { it != CaptchaTokenResult.Initial }
|
||||
.toToken()
|
||||
}
|
||||
}
|
||||
synchronized(tokenResult) {
|
||||
if (tokenResult.get().getOrNull() == null) {
|
||||
tokenResult.wait()
|
||||
} finally {
|
||||
AppSettings.setCaptchaResult(CaptchaTokenResult.Initial)
|
||||
AppSettings.setCaptchaRedirectUri(null)
|
||||
}
|
||||
|
||||
Log.d("Error14Interceptor", "passCaptchaAndGetToken: GET VALUE")
|
||||
tokenResult.get().getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun wrapResult(result: CaptchaTokenResult): Result<String> {
|
||||
return when (result) {
|
||||
// TODO: 03/05/2026, Danil Nikolaev: check again?
|
||||
CaptchaTokenResult.Null -> Result.success("")
|
||||
|
||||
CaptchaTokenResult.Cancelled, CaptchaTokenResult.Initial -> Result.success("")
|
||||
|
||||
is CaptchaTokenResult.Success -> Result.success(result.token)
|
||||
}
|
||||
private fun CaptchaTokenResult.toToken(): String = when (this) {
|
||||
is CaptchaTokenResult.Success -> token
|
||||
CaptchaTokenResult.Cancelled -> throw IOException("Captcha cancelled")
|
||||
CaptchaTokenResult.Null -> throw IOException("Captcha result is empty")
|
||||
CaptchaTokenResult.Initial -> throw IllegalStateException("Captcha result not ready")
|
||||
}
|
||||
|
||||
private fun Request.withSuccessToken(token: String): Request {
|
||||
@@ -133,13 +105,10 @@ class Error14HandlingInterceptor(
|
||||
private fun Request.withCookie(): Request {
|
||||
return newBuilder().apply { cookie.get()?.let { addHeader("Cookie", it) } }.build()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val CAPTCHA_ERROR_CODE = 14
|
||||
private const val CAPTCHA_ERROR_KIND = "need_captcha"
|
||||
private val CAPTCHA_TIMEOUT = 10.minutes
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
|
||||
private inline fun Any.wait() = (this as Object).wait()
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
|
||||
private inline fun Any.notify() = (this as Object).notify()
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
|
||||
private inline fun Any.notifyAll() = (this as Object).notifyAll()
|
||||
|
||||
Reference in New Issue
Block a user