Manhastro: Fix cookie lifetime (#9255)

* Fix cookie lifetime

* Move class to file

* Rename

* Use const
This commit is contained in:
Chopper 2025-06-15 03:14:22 -03:00 committed by Draff
parent a4347e9da1
commit 04a963a59a
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 146 additions and 92 deletions

View File

@ -3,7 +3,7 @@ ext {
extClass = '.Manhastro'
themePkg = 'madara'
baseUrl = 'https://manhastro.net'
overrideVersionCode = 7
overrideVersionCode = 8
isNsfw = true
}

View File

@ -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<String, String>
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()
}

View File

@ -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<Attempt>().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<String, String>
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"))
}
}