From c18a7963bfd427f5c2f24bf14038dc2607c833f9 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 18 May 2026 20:45:41 +0300 Subject: [PATCH] fix: harden captcha and long poll parsing --- .../dev/meloda/fast/data/VkGroupsMap.kt | 6 +- .../kotlin/dev/meloda/fast/data/VkUsersMap.kt | 6 +- .../meloda/fast/domain/LongPollEventParser.kt | 147 ++++++++---------- .../meloda/fast/domain/OAuthUseCaseImpl.kt | 31 ++-- .../interceptor/Error14HandlingInterceptor.kt | 101 +++++------- 5 files changed, 124 insertions(+), 167 deletions(-) diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/VkGroupsMap.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/VkGroupsMap.kt index 67b4cb19..b8ed0549 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/VkGroupsMap.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/VkGroupsMap.kt @@ -21,12 +21,10 @@ class VkGroupsMap( else map[abs(convo.id)] fun messageActionGroup(message: VkMessage): VkGroupDomain? = - if (message.actionMemberId == null || message.actionMemberId!! >= 0) null - else map[abs(message.actionMemberId!!)] + message.actionMemberId?.takeIf { it < 0 }?.let { map[abs(it)] } fun messageActionGroup(message: VkMessageData): VkGroupDomain? = - if (message.action?.memberId == null || message.action!!.memberId!! >= 0) null - else map[abs(message.action!!.memberId!!)] + message.action?.memberId?.takeIf { it < 0 }?.let { map[abs(it)] } fun messageGroup(message: VkMessage): VkGroupDomain? = if (!message.isGroup()) null diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/VkUsersMap.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/VkUsersMap.kt index de012887..d2ce86fd 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/VkUsersMap.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/VkUsersMap.kt @@ -20,12 +20,10 @@ class VkUsersMap( else map[convo.id] fun messageActionUser(message: VkMessage): VkUser? = - if (message.actionMemberId == null || message.actionMemberId!! <= 0) null - else map[message.actionMemberId] + message.actionMemberId?.takeIf { it > 0 }?.let(map::get) fun messageActionUser(message: VkMessageData): VkUser? = - if (message.action?.memberId == null || message.action!!.memberId!! <= 0) null - else map[message.action!!.memberId] + message.action?.memberId?.takeIf { it > 0 }?.let(map::get) fun messageUser(message: VkMessage): VkUser? = if (!message.isUser()) null diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollEventParser.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollEventParser.kt index d2db1a87..52a1750c 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollEventParser.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollEventParser.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine internal class LongPollEventParser( private val coroutineScope: CoroutineScope, @@ -35,7 +35,7 @@ internal class LongPollEventParser( val eventId = event.first().asInt() when (val eventType = ApiEvent.parseOrNull(eventId)) { - null -> Log.d("LongPollEventParser", "parseNextUpdate: unknownEvent: $event") + null -> Unit ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) @@ -62,8 +62,6 @@ internal class LongPollEventParser( } private fun parseMessageSetFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val cmId = event[1].asLong() val flags = event[2].asInt() val peerId = event[3].asLong() @@ -128,8 +126,6 @@ internal class LongPollEventParser( } private fun parseMessageClearFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val cmId = event[1].asLong() val flags = event[2].asInt() val peerId = event[3].asLong() @@ -192,8 +188,6 @@ internal class LongPollEventParser( } private fun parseMessageNew(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val cmId = event[1].asLong() val peerId = event[4].asLong() @@ -223,8 +217,6 @@ internal class LongPollEventParser( } private fun parseMessageEdit(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val cmId = event[1].asLong() val peerId = event[3].asLong() @@ -239,40 +231,28 @@ internal class LongPollEventParser( } private fun parseMessageReadIncoming(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val peerId = event[1].asLong() - val cmId = event[2].asLong() - val unreadCount = event[3].asInt() - - dispatch( - LongPollEvent.INCOMING_MESSAGE_READ, - LongPollParsedEvent.IncomingMessageRead( - peerId = peerId, - cmId = cmId, - unreadCount = unreadCount + dispatchMessageRead( + longPollEvent = LongPollEvent.INCOMING_MESSAGE_READ, + parsedEvent = LongPollParsedEvent.IncomingMessageRead( + peerId = event[1].asLong(), + cmId = event[2].asLong(), + unreadCount = event[3].asInt() ) ) } private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val peerId = event[1].asLong() - val cmId = event[2].asLong() - val unreadCount = event[3].asInt() - - dispatch( - LongPollEvent.OUTGOING_MESSAGE_READ, - LongPollParsedEvent.OutgoingMessageRead( - peerId = peerId, - cmId = cmId, - unreadCount = unreadCount + dispatchMessageRead( + longPollEvent = LongPollEvent.OUTGOING_MESSAGE_READ, + parsedEvent = LongPollParsedEvent.OutgoingMessageRead( + peerId = event[1].asLong(), + cmId = event[2].asLong(), + unreadCount = event[3].asInt() ) ) } private fun parseChatClearFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val peerId = event[1].asLong() val flags = event[2].asInt() @@ -284,23 +264,11 @@ internal class LongPollEventParser( parsedFlags.forEach { flag -> when (flag) { ConvoFlags.ARCHIVED -> { - val convo = loadConvo( + handleArchivedChat( peerId = peerId, - extended = true, - fields = VkConstants.ALL_FIELDS - ) ?: return@forEach - - val message = loadMessage( - peerId = peerId, - cmId = convo.lastCmId + archived = false, + eventsToSend = eventsToSend ) - - val eventToSend = LongPollParsedEvent.ChatArchived( - convo = convo.copy(lastMessage = message), - archived = false - ) - eventsToSend += eventToSend - dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend) } else -> Unit @@ -312,8 +280,6 @@ internal class LongPollEventParser( } private fun parseChatSetFlags(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val peerId = event[1].asLong() val flags = event[2].asInt() @@ -325,23 +291,11 @@ internal class LongPollEventParser( parsedFlags.forEach { flag -> when (flag) { ConvoFlags.ARCHIVED -> { - val convo = loadConvo( + handleArchivedChat( peerId = peerId, - extended = true, - fields = VkConstants.ALL_FIELDS - ) ?: return@forEach - - val message = loadMessage( - peerId = peerId, - cmId = convo.lastCmId + archived = true, + eventsToSend = eventsToSend ) - - val eventToSend = LongPollParsedEvent.ChatArchived( - convo = convo.copy(lastMessage = message), - archived = true - ) - eventsToSend += eventToSend - dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend) } else -> Unit @@ -353,8 +307,6 @@ internal class LongPollEventParser( } private fun parseMessagesDeleted(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val peerId = event[1].asLong() val cmId = event[2].asLong() @@ -368,8 +320,6 @@ internal class LongPollEventParser( } private fun parseChatMajorChanged(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val peerId = event[1].asLong() val majorId = event[2].asInt() @@ -383,8 +333,6 @@ internal class LongPollEventParser( } private fun parseChatMinorChanged(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val peerId = event[1].asLong() val minorId = event[2].asInt() @@ -398,8 +346,6 @@ internal class LongPollEventParser( } private fun parseInteraction(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType: $event") - val interactionType = when (eventType) { ApiEvent.TYPING -> InteractionType.Typing ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage @@ -438,8 +384,6 @@ internal class LongPollEventParser( } private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType $event") - val unreadCount = event[1].asInt() val unreadUnmutedCount = event[2].asInt() val showOnlyMuted = event[3].asInt() == 1 @@ -463,8 +407,6 @@ internal class LongPollEventParser( } private fun parseMessageUpdated(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType $event") - val cmId = event[1].asLong() val peerId = event[4].asLong() @@ -479,8 +421,6 @@ internal class LongPollEventParser( } private fun parseMessageCacheClear(eventType: ApiEvent, event: List) { - Log.d("LongPollEventParser", "$eventType $event") - val messageId = event[1].asLong() coroutineScope.launch(Dispatchers.IO) { @@ -497,10 +437,10 @@ internal class LongPollEventParser( peerId: Long? = null, cmId: Long? = null, messageId: Long? = null - ): VkMessage? = suspendCoroutine { continuation -> + ): VkMessage? = suspendCancellableCoroutine { continuation -> require((peerId != null && cmId != null) || messageId != null) - coroutineScope.launch(Dispatchers.IO) { + val job = coroutineScope.launch(Dispatchers.IO) { messagesUseCase.getById( peerCmIds = null, peerId = peerId, @@ -525,14 +465,18 @@ internal class LongPollEventParser( ) } } + + continuation.invokeOnCancellation { + job.cancel() + } } private suspend fun loadConvo( peerId: Long, extended: Boolean = false, fields: String? = null - ): VkConvo? = suspendCoroutine { continuation -> - coroutineScope.launch(Dispatchers.IO) { + ): VkConvo? = suspendCancellableCoroutine { continuation -> + val job = coroutineScope.launch(Dispatchers.IO) { convoUseCase.getById( peerIds = listOf(peerId), extended = extended, @@ -554,5 +498,40 @@ internal class LongPollEventParser( ) } } + + continuation.invokeOnCancellation { + job.cancel() + } + } + + private suspend fun handleArchivedChat( + peerId: Long, + archived: Boolean, + eventsToSend: MutableList + ) { + val convo = loadConvo( + peerId = peerId, + extended = true, + fields = VkConstants.ALL_FIELDS + ) ?: return + + val message = loadMessage( + peerId = peerId, + cmId = convo.lastCmId + ) + + val eventToSend = LongPollParsedEvent.ChatArchived( + convo = convo.copy(lastMessage = message), + archived = archived + ) + eventsToSend += eventToSend + dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend) + } + + private fun dispatchMessageRead( + longPollEvent: LongPollEvent, + parsedEvent: LongPollParsedEvent + ) { + dispatch(longPollEvent, parsedEvent) } } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCaseImpl.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCaseImpl.kt index e848e5a6..d6604e8f 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCaseImpl.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/OAuthUseCaseImpl.kt @@ -22,22 +22,35 @@ class OAuthUseCaseImpl( ): Flow> = flow { emit(State.Loading) - val newState = oAuthRepository.auth( + val newState = when (val authResult = oAuthRepository.auth( login = login, password = password, forceSms = forceSms, validationCode = validationCode, captchaSid = captchaSid, captchaKey = captchaKey - ).asState( - successMapper = { - AuthInfo( - userId = it.userId!!, - accessToken = it.accessToken!!, - validationHash = it.validationHash!! - ) + )) { + is com.slack.eithernet.ApiResult.Success -> { + val value = authResult.value + val userId = value.userId + val accessToken = value.accessToken + val validationHash = value.validationHash + + if (userId == null || accessToken == null || validationHash == null) { + State.Error.InternalError + } else { + State.Success( + AuthInfo( + userId = userId, + accessToken = accessToken, + validationHash = validationHash + ) + ) + } } - ) + + else -> authResult.asState() + } emit(newState) } diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/interceptor/Error14HandlingInterceptor.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/interceptor/Error14HandlingInterceptor.kt index 8e54710b..db80dc55 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/interceptor/Error14HandlingInterceptor.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/interceptor/Error14HandlingInterceptor.kt @@ -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 = emptySet(), -) : Interceptor { +class Error14HandlingInterceptor : Interceptor { private val cookie = AtomicReference(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.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 { - 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()