From 04a963a59a3f1be3ae222b695007ef059defe3b6 Mon Sep 17 00:00:00 2001 From: Chopper <156493704+choppeh@users.noreply.github.com> Date: Sun, 15 Jun 2025 03:14:22 -0300 Subject: [PATCH] Manhastro: Fix cookie lifetime (#9255) * Fix cookie lifetime * Move class to file * Rename * Use const --- src/pt/manhastro/build.gradle | 2 +- .../tachiyomi/extension/pt/manhastro/Auth.kt | 92 +++++++++++ .../extension/pt/manhastro/Manhastro.kt | 144 +++++++----------- 3 files changed, 146 insertions(+), 92 deletions(-) create mode 100644 src/pt/manhastro/src/eu/kanade/tachiyomi/extension/pt/manhastro/Auth.kt diff --git a/src/pt/manhastro/build.gradle b/src/pt/manhastro/build.gradle index caaf15b5c..b87f9c8a1 100644 --- a/src/pt/manhastro/build.gradle +++ b/src/pt/manhastro/build.gradle @@ -3,7 +3,7 @@ ext { extClass = '.Manhastro' themePkg = 'madara' baseUrl = 'https://manhastro.net' - overrideVersionCode = 7 + overrideVersionCode = 8 isNsfw = true } diff --git a/src/pt/manhastro/src/eu/kanade/tachiyomi/extension/pt/manhastro/Auth.kt b/src/pt/manhastro/src/eu/kanade/tachiyomi/extension/pt/manhastro/Auth.kt new file mode 100644 index 000000000..1224450ca --- /dev/null +++ b/src/pt/manhastro/src/eu/kanade/tachiyomi/extension/pt/manhastro/Auth.kt @@ -0,0 +1,92 @@ +package eu.kanade.tachiyomi.extension.pt.manhastro + +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +@Serializable +class Cookie { + val value: Pair + val expired: String + + private constructor() { + value = "" to "" + expired = "" + } + + constructor(setCookie: String) { + val slices = setCookie.split("; ") + value = slices.first().split("=").let { + it.first() to it.last() + } + expired = Calendar.getInstance().apply { + time = Date() + add(Calendar.DATE, EXPIRE_AT) + }.let { dateFormat.format(it.time) } + } + + fun isExpired(): Boolean = + try { dateFormat.parse(expired)!!.before(Date()) } catch (e: Exception) { true } + + fun isEmpty(): Boolean = expired.isEmpty() || value.toList().any(String::isBlank) + + companion object { + fun empty() = Cookie() + private val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.ROOT) + private const val EXPIRE_AT = 2 + } +} + +@Serializable +class Attempt( + private var attempts: Long = 0, + private var updateAt: Long = Date().time, +) { + fun takeIfUnlocked(): Long? { + if (hasNext()) { + return attempts.also { + register() + } + } + + return attempts.takeIf { now.isAfter(lockPeriod) }?.let { + reset() + register() + } + } + + private fun Date.isAfter(date: Date) = this.after(date) + private val now: Date get() = Date() + private val lockPeriod: Date get() = Calendar.getInstance().apply { + time = Date(updateAt) + add(Calendar.HOUR, MIN_PERIOD) + }.time + + fun reset() { + attempts = 0 + } + + fun updateAt(): String = dateFormat.format(Date(updateAt)) + + fun hasNext(): Boolean = attempts <= MAX_ATTEMPT_WITHIN_PERIOD + + fun register() = (attempts++).also { updateAt = Date().time } + + override fun toString(): String = attempts.toString() + + companion object { + const val MIN_PERIOD = 6 + private const val MAX_ATTEMPT_WITHIN_PERIOD = 2 + private val dateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale("pt", "BR")) + } +} + +class Credential( + val email: String, + val password: String, +) { + val isEmpty: Boolean get() = email.isBlank() || password.isBlank() + val isNotEmpty: Boolean get() = isEmpty.not() +} diff --git a/src/pt/manhastro/src/eu/kanade/tachiyomi/extension/pt/manhastro/Manhastro.kt b/src/pt/manhastro/src/eu/kanade/tachiyomi/extension/pt/manhastro/Manhastro.kt index ae9c2f8da..8cc69cdf1 100644 --- a/src/pt/manhastro/src/eu/kanade/tachiyomi/extension/pt/manhastro/Manhastro.kt +++ b/src/pt/manhastro/src/eu/kanade/tachiyomi/extension/pt/manhastro/Manhastro.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.os.Handler import android.os.Looper import android.util.Base64 +import android.util.Log import android.widget.Toast import androidx.preference.EditTextPreference import androidx.preference.PreferenceScreen @@ -18,7 +19,6 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.util.asJsoup import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs -import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import okhttp3.CacheControl @@ -33,10 +33,9 @@ import org.jsoup.nodes.Document import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date import java.util.Locale import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean class Manhastro : Madara( @@ -67,10 +66,17 @@ class Manhastro : ?: doAuth().also(::upsetCookie) } + // Prevent multiple login when rateLimit is greater than 1 + private var initializedCookie = AtomicBoolean(false) private val cookieInterceptor: Interceptor by lazy { return@lazy when { authCookie.isEmpty() -> bypassInterceptor - else -> CookieInterceptor(baseUrl.substringAfterLast("/"), listOf(authCookie.value)) + else -> { + if (initializedCookie.compareAndSet(false, true).not()) { + return@lazy bypassInterceptor + } + CookieInterceptor(baseUrl.substringAfterLast("/"), listOf(authCookie.value)) + } } } @@ -182,62 +188,52 @@ class Manhastro : password = preferences.getString(PASSWORD_PREF, "") as String, ) - private val defaultClient = network.cloudflareClient + private val defaultClient = network.cloudflareClient.newBuilder() + .rateLimit(1) + .readTimeout(1, TimeUnit.MINUTES) + .connectTimeout(1, TimeUnit.MINUTES) + .build() - var attempts: Int - get() = preferences.getInt(COOKIE_ATTEMPT, 0) - set(value) { - preferences.edit() - .putInt(COOKIE_ATTEMPT, value) - .apply() - } - var lastAttempt: Date? + private var _cache: Attempt? = null + val attempt: Attempt get() { - val time = preferences.getLong(COOKIE_LAST_REQUEST, 0) - if (time == 0L) { - return null + if (_cache != null) { + return _cache!! } - return Date(time) - } - set(value) { - val time = value?.time ?: 0L - preferences.edit() - .putLong(COOKIE_LAST_REQUEST, time) - .apply() + + val stored = preferences.getString(COOKIE_ATTEMPT_REF, "") + if (stored.isNullOrBlank()) { + return Attempt().also { + _cache = it + preferences + .edit() + .putString(COOKIE_ATTEMPT_REF, json.encodeToString(it)) + .apply() + } + } + return stored.parseAs().also { _cache = it } } + private fun Attempt.save() { + preferences + .edit() + .putString(COOKIE_ATTEMPT_REF, json.encodeToString(this)) + .apply() + } + private fun doAuth(): Cookie { if (credentials.isEmpty) { return Cookie.empty() } - attempts++ - /** - * Avoids excessive invalid requests when credentials are invalid - */ - if (attempts >= MAX_ATTEMPT_WITHIN_PERIOD) { - if (lastAttempt == null) { - lastAttempt = Date() - return Cookie.empty() - } - - val lockPeriod = Calendar.getInstance().apply { - time = lastAttempt!! - add(Calendar.HOUR, 6) - } - - when { - Date().after(lockPeriod.time) -> { - attempts = 0 - lastAttempt = null - } - else -> { - showToast("Login permitido após 6h de ${toastDateFormat.format(lastAttempt!!)}") - return Cookie.empty() - } - } + val attemptCount = attempt.takeIfUnlocked() ?: return Cookie.empty().also { + showToast("Login permitido após ${Attempt.MIN_PERIOD}h de ${attempt.updateAt()}") } + Log.i(Manhastro::class.simpleName, "trying: ${attemptCount}x") + + attempt.save() + val document = defaultClient.newCall(GET(baseUrl, headers)).execute().asJsoup() val nonce = document.selectFirst("script#wp-manga-login-ajax-js-extra") ?.data() @@ -249,17 +245,22 @@ class Manhastro : val formHeaders = headers.newBuilder() .set("Accept", "*/*") .set("Accept-Encoding", "gzip, deflate, br") - .set("Accept-Language", "en-US,en;q=0.9") + .set("Accept-Language", "pt-BR,en-US;q=0.7,en;q=0.3") .set("Connection", "keep-alive") .set("Origin", baseUrl) .set("Referer", "$baseUrl/") + .set("Sec-Fetch-Site", "same-origin") + .set("Sec-Fetch-Mode", "cors") + .set("Sec-Fetch-Dest", "empty") + .set("Priority", "u=0") + .set("TE", "trailers") .set("X-Requested-With", "XMLHttpRequest") .build() val form = FormBody.Builder() .add("action", "wp_manga_signin") .add("login", credentials.email) - .add("pass", credentials.password) + .addEncoded("pass", credentials.password) .add("rememberme", "forever") .add("nonce", nonce) .build() @@ -276,6 +277,7 @@ class Manhastro : Falha ao acessar recurso: Usuário ou senha incorreto. Altere suas credências em Extensões > $name > Configuração. """.trimIndent() + response.use { if (it.isSuccessful.not()) { showToast(message) @@ -290,43 +292,6 @@ class Manhastro : } } - private class Credential( - val email: String, - val password: String, - ) { - val isEmpty: Boolean get() = email.isBlank() || password.isBlank() - val isNotEmpty: Boolean get() = isEmpty.not() - } - - @Serializable - private class Cookie { - val value: Pair - val expired: String - - private constructor() { - value = "" to "" - expired = "" - } - - constructor(setCookie: String) { - val slices = setCookie.split("; ") - value = slices.first().split("=").let { - it.first() to it.last() - } - expired = slices.last().substringAfter("=") - } - - fun isExpired(): Boolean = - try { dateFormat.parse(expired)!!.before(Date()) } catch (e: Exception) { true } - - fun isEmpty(): Boolean = expired.isEmpty() || value.toList().any(String::isBlank) - - companion object { - fun empty() = Cookie() - private val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH) - } - } - // ============================ Utilities ==================================== private fun OkHttpClient.Builder.addNetworkInterceptorIf( @@ -363,15 +328,12 @@ class Manhastro : } companion object { - private const val MAX_ATTEMPT_WITHIN_PERIOD = 2 private const val RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações" private const val USERNAME_PREF = "MANHASTRO_USERNAME" private const val PASSWORD_PREF = "MANHASTRO_PASSWORD" private const val COOKIE_STORAGE_PREF = "MANHASTRO_COOKIE_STORAGE" - private const val COOKIE_LAST_REQUEST = "MANHASTRO_COOKIE_LAST_REQUEST" - private const val COOKIE_ATTEMPT = "MANHASTRO_COOKIE_ATTEMPT" + private const val COOKIE_ATTEMPT_REF = "MANHASTRO_COOKIE_ATTEMPT_REF" private const val IMG_CONTENT_TYPE = "image/jpeg" private val NONCE_LOGIN_REGEX = """"nonce":"([^"]+)""".toRegex() - val toastDateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale("pt", "BR")) } }