Manhastro: Fix cookie lifetime (#9255)
* Fix cookie lifetime * Move class to file * Rename * Use const
This commit is contained in:
parent
a4347e9da1
commit
04a963a59a
@ -3,7 +3,7 @@ ext {
|
|||||||
extClass = '.Manhastro'
|
extClass = '.Manhastro'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://manhastro.net'
|
baseUrl = 'https://manhastro.net'
|
||||||
overrideVersionCode = 7
|
overrideVersionCode = 8
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -5,6 +5,7 @@ import android.content.SharedPreferences
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
@ -18,7 +19,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import keiyoushi.utils.getPreferences
|
import keiyoushi.utils.getPreferences
|
||||||
import keiyoushi.utils.parseAs
|
import keiyoushi.utils.parseAs
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
@ -33,10 +33,9 @@ import org.jsoup.nodes.Document
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class Manhastro :
|
class Manhastro :
|
||||||
Madara(
|
Madara(
|
||||||
@ -67,10 +66,17 @@ class Manhastro :
|
|||||||
?: doAuth().also(::upsetCookie)
|
?: doAuth().also(::upsetCookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent multiple login when rateLimit is greater than 1
|
||||||
|
private var initializedCookie = AtomicBoolean(false)
|
||||||
private val cookieInterceptor: Interceptor by lazy {
|
private val cookieInterceptor: Interceptor by lazy {
|
||||||
return@lazy when {
|
return@lazy when {
|
||||||
authCookie.isEmpty() -> bypassInterceptor
|
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,27 +188,36 @@ class Manhastro :
|
|||||||
password = preferences.getString(PASSWORD_PREF, "") as String,
|
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
|
private var _cache: Attempt? = null
|
||||||
get() = preferences.getInt(COOKIE_ATTEMPT, 0)
|
val attempt: Attempt
|
||||||
set(value) {
|
get() {
|
||||||
preferences.edit()
|
if (_cache != null) {
|
||||||
.putInt(COOKIE_ATTEMPT, value)
|
return _cache!!
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
.apply()
|
||||||
}
|
}
|
||||||
var lastAttempt: Date?
|
|
||||||
get() {
|
|
||||||
val time = preferences.getLong(COOKIE_LAST_REQUEST, 0)
|
|
||||||
if (time == 0L) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
return Date(time)
|
return stored.parseAs<Attempt>().also { _cache = it }
|
||||||
}
|
}
|
||||||
set(value) {
|
|
||||||
val time = value?.time ?: 0L
|
private fun Attempt.save() {
|
||||||
preferences.edit()
|
preferences
|
||||||
.putLong(COOKIE_LAST_REQUEST, time)
|
.edit()
|
||||||
|
.putString(COOKIE_ATTEMPT_REF, json.encodeToString(this))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,32 +226,13 @@ class Manhastro :
|
|||||||
return Cookie.empty()
|
return Cookie.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts++
|
val attemptCount = attempt.takeIfUnlocked() ?: return Cookie.empty().also {
|
||||||
/**
|
showToast("Login permitido após ${Attempt.MIN_PERIOD}h de ${attempt.updateAt()}")
|
||||||
* 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 {
|
Log.i(Manhastro::class.simpleName, "trying: ${attemptCount}x")
|
||||||
time = lastAttempt!!
|
|
||||||
add(Calendar.HOUR, 6)
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
attempt.save()
|
||||||
Date().after(lockPeriod.time) -> {
|
|
||||||
attempts = 0
|
|
||||||
lastAttempt = null
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
showToast("Login permitido após 6h de ${toastDateFormat.format(lastAttempt!!)}")
|
|
||||||
return Cookie.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val document = defaultClient.newCall(GET(baseUrl, headers)).execute().asJsoup()
|
val document = defaultClient.newCall(GET(baseUrl, headers)).execute().asJsoup()
|
||||||
val nonce = document.selectFirst("script#wp-manga-login-ajax-js-extra")
|
val nonce = document.selectFirst("script#wp-manga-login-ajax-js-extra")
|
||||||
@ -249,17 +245,22 @@ class Manhastro :
|
|||||||
val formHeaders = headers.newBuilder()
|
val formHeaders = headers.newBuilder()
|
||||||
.set("Accept", "*/*")
|
.set("Accept", "*/*")
|
||||||
.set("Accept-Encoding", "gzip, deflate, br")
|
.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("Connection", "keep-alive")
|
||||||
.set("Origin", baseUrl)
|
.set("Origin", baseUrl)
|
||||||
.set("Referer", "$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")
|
.set("X-Requested-With", "XMLHttpRequest")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val form = FormBody.Builder()
|
val form = FormBody.Builder()
|
||||||
.add("action", "wp_manga_signin")
|
.add("action", "wp_manga_signin")
|
||||||
.add("login", credentials.email)
|
.add("login", credentials.email)
|
||||||
.add("pass", credentials.password)
|
.addEncoded("pass", credentials.password)
|
||||||
.add("rememberme", "forever")
|
.add("rememberme", "forever")
|
||||||
.add("nonce", nonce)
|
.add("nonce", nonce)
|
||||||
.build()
|
.build()
|
||||||
@ -276,6 +277,7 @@ class Manhastro :
|
|||||||
Falha ao acessar recurso: Usuário ou senha incorreto.
|
Falha ao acessar recurso: Usuário ou senha incorreto.
|
||||||
Altere suas credências em Extensões > $name > Configuração.
|
Altere suas credências em Extensões > $name > Configuração.
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
response.use {
|
response.use {
|
||||||
if (it.isSuccessful.not()) {
|
if (it.isSuccessful.not()) {
|
||||||
showToast(message)
|
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 ====================================
|
// ============================ Utilities ====================================
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.addNetworkInterceptorIf(
|
private fun OkHttpClient.Builder.addNetworkInterceptorIf(
|
||||||
@ -363,15 +328,12 @@ class Manhastro :
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações"
|
||||||
private const val USERNAME_PREF = "MANHASTRO_USERNAME"
|
private const val USERNAME_PREF = "MANHASTRO_USERNAME"
|
||||||
private const val PASSWORD_PREF = "MANHASTRO_PASSWORD"
|
private const val PASSWORD_PREF = "MANHASTRO_PASSWORD"
|
||||||
private const val COOKIE_STORAGE_PREF = "MANHASTRO_COOKIE_STORAGE"
|
private const val COOKIE_STORAGE_PREF = "MANHASTRO_COOKIE_STORAGE"
|
||||||
private const val COOKIE_LAST_REQUEST = "MANHASTRO_COOKIE_LAST_REQUEST"
|
private const val COOKIE_ATTEMPT_REF = "MANHASTRO_COOKIE_ATTEMPT_REF"
|
||||||
private const val COOKIE_ATTEMPT = "MANHASTRO_COOKIE_ATTEMPT"
|
|
||||||
private const val IMG_CONTENT_TYPE = "image/jpeg"
|
private const val IMG_CONTENT_TYPE = "image/jpeg"
|
||||||
private val NONCE_LOGIN_REGEX = """"nonce":"([^"]+)""".toRegex()
|
private val NONCE_LOGIN_REGEX = """"nonce":"([^"]+)""".toRegex()
|
||||||
val toastDateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale("pt", "BR"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user