MangaHub: try to refresh api key for all api requests (#8659)
* MangaHub: try to refresh api key for all api requests * update in interceptor * remove logs
This commit is contained in:
parent
a56eb29dec
commit
e3cbc49e38
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 33
|
baseVersionCode = 34
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection UseTomlInstead
|
//noinspection UseTomlInstead
|
||||||
|
@ -29,14 +29,15 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import org.brotli.dec.BrotliInputStream
|
import org.brotli.dec.BrotliInputStream
|
||||||
import rx.Observable
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
abstract class MangaHub(
|
abstract class MangaHub(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
@ -53,6 +54,7 @@ abstract class MangaHub(
|
|||||||
private val baseThumbCdnUrl = "https://thumb.mghcdn.com"
|
private val baseThumbCdnUrl = "https://thumb.mghcdn.com"
|
||||||
private val apiRegex = Regex("mhub_access=([^;]+)")
|
private val apiRegex = Regex("mhub_access=([^;]+)")
|
||||||
private val spaceRegex = Regex("\\s+")
|
private val spaceRegex = Regex("\\s+")
|
||||||
|
private val apiErrorRegex = Regex("""rate\s*limit|api\s*key""")
|
||||||
|
|
||||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||||
|
|
||||||
@ -63,7 +65,6 @@ abstract class MangaHub(
|
|||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(::apiAuthInterceptor)
|
.addInterceptor(::apiAuthInterceptor)
|
||||||
.addInterceptor(::graphQLApiInterceptor)
|
|
||||||
.addNetworkInterceptor(::compatEncodingInterceptor)
|
.addNetworkInterceptor(::compatEncodingInterceptor)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ abstract class MangaHub(
|
|||||||
.add("Sec-Fetch-Site", "same-origin")
|
.add("Sec-Fetch-Site", "same-origin")
|
||||||
.add("Upgrade-Insecure-Requests", "1")
|
.add("Upgrade-Insecure-Requests", "1")
|
||||||
|
|
||||||
private fun postRequestGraphQL(query: String): Request {
|
private fun postRequestGraphQL(query: String, refreshUrl: String? = null): Request {
|
||||||
val requestHeaders = headersBuilder()
|
val requestHeaders = headersBuilder()
|
||||||
.set("Accept", "application/json")
|
.set("Accept", "application/json")
|
||||||
.set("Content-Type", "application/json")
|
.set("Content-Type", "application/json")
|
||||||
@ -94,29 +95,10 @@ abstract class MangaHub(
|
|||||||
|
|
||||||
return POST("$baseApiUrl/graphql", requestHeaders, body.toString().toRequestBody())
|
return POST("$baseApiUrl/graphql", requestHeaders, body.toString().toRequestBody())
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.tag(GraphQLTag())
|
.tag(GraphQLTag::class.java, GraphQLTag(refreshUrl))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
|
|
||||||
val cookie = client.cookieJar
|
|
||||||
.loadForRequest(baseUrl.toHttpUrl())
|
|
||||||
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }
|
|
||||||
|
|
||||||
val request =
|
|
||||||
if (originalRequest.url.toString() == "$baseApiUrl/graphql" && cookie != null) {
|
|
||||||
originalRequest.newBuilder()
|
|
||||||
.header("x-mhub-access", cookie.value)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
originalRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normally this gets handled properly but in older forks such as TachiyomiJ2K, we have to manually intercept it
|
// Normally this gets handled properly but in older forks such as TachiyomiJ2K, we have to manually intercept it
|
||||||
// as they have an outdated implementation of NetworkHelper.
|
// as they have an outdated implementation of NetworkHelper.
|
||||||
private fun compatEncodingInterceptor(chain: Interceptor.Chain): Response {
|
private fun compatEncodingInterceptor(chain: Interceptor.Chain): Response {
|
||||||
@ -155,63 +137,99 @@ abstract class MangaHub(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun graphQLApiInterceptor(chain: Interceptor.Chain): Response {
|
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
|
val tag = request.tag(GraphQLTag::class.java)
|
||||||
|
?: return chain.proceed(request) // We won't intercept non-graphql requests (like image retrieval)
|
||||||
|
|
||||||
// We won't intercept non-graphql requests (like image retrieval)
|
return try {
|
||||||
if (!request.hasGraphQLTag()) {
|
tryApiRequest(chain, request)
|
||||||
return chain.proceed(request)
|
} catch (e: Throwable) {
|
||||||
|
val noCookie = e is MangaHubCookieNotFound
|
||||||
|
val apiError = e is ApiErrorException &&
|
||||||
|
apiErrorRegex.containsMatchIn(e.message ?: "")
|
||||||
|
|
||||||
|
if (noCookie || apiError) {
|
||||||
|
refreshApiKey(tag.refreshUrl)
|
||||||
|
tryApiRequest(chain, request)
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val response = chain.proceed(request)
|
private fun tryApiRequest(chain: Interceptor.Chain, request: Request): Response {
|
||||||
|
val cookie = client.cookieJar
|
||||||
|
.loadForRequest(baseUrl.toHttpUrl())
|
||||||
|
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }
|
||||||
|
?: throw MangaHubCookieNotFound()
|
||||||
|
|
||||||
|
val apiRequest = request.newBuilder()
|
||||||
|
.header("x-mhub-access", cookie.value)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = chain.proceed(apiRequest)
|
||||||
|
|
||||||
|
val apiResponse = response.peekBody(Long.MAX_VALUE).string()
|
||||||
|
.parseAs<ApiResponseError>()
|
||||||
|
|
||||||
// We don't care about the data, only the possible error associated with it
|
|
||||||
// If we encounter an error, we'll intercept it and throw an error for app to catch
|
|
||||||
val apiResponse = response.peekBody(Long.MAX_VALUE).string().parseAs<ApiResponseError>()
|
|
||||||
if (apiResponse.errors != null) {
|
if (apiResponse.errors != null) {
|
||||||
response.close() // Avoid leaks
|
response.close() // Avoid leaks
|
||||||
val errors = apiResponse.errors.joinToString("\n") { it.message }
|
val errors = apiResponse.errors.joinToString("\n") { it.message }
|
||||||
throw IOException(errors)
|
throw ApiErrorException(errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything works fine
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Request.hasGraphQLTag(): Boolean {
|
private class MangaHubCookieNotFound : IOException("mhub_access cookie not found")
|
||||||
return this.tag() is GraphQLTag
|
private class ApiErrorException(errorMessage: String) : IOException(errorMessage)
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshApiKey(chapter: SChapter) {
|
private val lock = ReentrantLock()
|
||||||
val url = "$baseUrl/chapter${chapter.url}".toHttpUrl()
|
private var refreshed = 0L
|
||||||
val oldKey = client.cookieJar
|
|
||||||
.loadForRequest(baseUrl.toHttpUrl())
|
|
||||||
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
|
|
||||||
|
|
||||||
for (i in 1..2) {
|
private fun refreshApiKey(refreshUrl: String? = null) {
|
||||||
// Clear key cookie
|
if (refreshed + 10000 < System.currentTimeMillis() && lock.tryLock()) {
|
||||||
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
|
val url = when {
|
||||||
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
refreshUrl != null -> refreshUrl
|
||||||
|
else -> "$baseUrl/chapter/martial-peak/chapter-${Random.nextInt(1000, 3000)}"
|
||||||
|
}.toHttpUrl()
|
||||||
|
|
||||||
// We try requesting again with param if the first one fails
|
val oldKey = client.cookieJar
|
||||||
val query = if (i == 2) "?reloadKey=1" else ""
|
.loadForRequest(baseUrl.toHttpUrl())
|
||||||
|
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
|
||||||
|
|
||||||
try {
|
for (i in 1..2) {
|
||||||
val response = client.newCall(
|
// Clear key cookie
|
||||||
GET(
|
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
|
||||||
"$url$query",
|
client.cookieJar.saveFromResponse(url, listOf(cookie))
|
||||||
headers.newBuilder()
|
|
||||||
.set("Referer", "$baseUrl/manga/${url.pathSegments[1]}")
|
|
||||||
.build(),
|
|
||||||
),
|
|
||||||
).execute()
|
|
||||||
val returnedKey = response.headers["set-cookie"]?.let { apiRegex.find(it)?.groupValues?.get(1) }
|
|
||||||
response.close() // Avoid potential resource leaks
|
|
||||||
|
|
||||||
if (returnedKey != oldKey) break // Break out of loop since we got an allegedly valid API key
|
try {
|
||||||
} catch (_: IOException) {
|
// We try requesting again with param if the first one fails
|
||||||
throw IOException("An error occurred while obtaining a new API key") // Show error
|
val query = if (i == 2) "?reloadKey=1" else ""
|
||||||
|
val response = client.newCall(
|
||||||
|
GET(
|
||||||
|
"$url$query",
|
||||||
|
headers.newBuilder()
|
||||||
|
.set("Referer", "$baseUrl/manga/${url.pathSegments[1]}")
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
).execute()
|
||||||
|
val returnedKey =
|
||||||
|
response.headers["set-cookie"]?.let { apiRegex.find(it)?.groupValues?.get(1) }
|
||||||
|
response.close() // Avoid potential resource leaks
|
||||||
|
|
||||||
|
if (returnedKey != oldKey) break // Break out of loop since we got an allegedly valid API key
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
lock.unlock()
|
||||||
|
throw Exception("An error occurred while obtaining a new API key") // Show error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
refreshed = System.currentTimeMillis()
|
||||||
|
lock.unlock()
|
||||||
|
} else {
|
||||||
|
lock.lock() // wait here until lock is released
|
||||||
|
lock.unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +318,10 @@ abstract class MangaHub(
|
|||||||
|
|
||||||
// manga details
|
// manga details
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
return postRequestGraphQL(mangaDetailsQuery(mangaSource, manga.url.removePrefix("/manga/")))
|
return postRequestGraphQL(
|
||||||
|
mangaDetailsQuery(mangaSource, manga.url.removePrefix("/manga/")),
|
||||||
|
refreshUrl = "$baseUrl${manga.url}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
@ -335,7 +356,10 @@ abstract class MangaHub(
|
|||||||
|
|
||||||
// Chapters
|
// Chapters
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
return postRequestGraphQL(mangaChapterListQuery(mangaSource, manga.url.removePrefix("/manga/")))
|
return postRequestGraphQL(
|
||||||
|
mangaChapterListQuery(mangaSource, manga.url.removePrefix("/manga/")),
|
||||||
|
refreshUrl = "$baseUrl${manga.url}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
@ -379,21 +403,10 @@ abstract class MangaHub(
|
|||||||
override fun pageListRequest(chapter: SChapter): Request {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
val chapterUrl = chapter.url.split("/")
|
val chapterUrl = chapter.url.split("/")
|
||||||
|
|
||||||
return postRequestGraphQL(pagesQuery(mangaSource, chapterUrl[1], chapterUrl[2].substringAfter("-").toFloat()))
|
return postRequestGraphQL(
|
||||||
}
|
pagesQuery(mangaSource, chapterUrl[1], chapterUrl[2].substringAfter("-").toFloat()),
|
||||||
|
refreshUrl = "$baseUrl/chapter${chapter.url}",
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
)
|
||||||
var doApiRefresh = true
|
|
||||||
|
|
||||||
return super.fetchPageList(chapter)
|
|
||||||
.doOnError {
|
|
||||||
// Ensure that the api refresh call will happen only once
|
|
||||||
if (doApiRefresh) {
|
|
||||||
refreshApiKey(chapter)
|
|
||||||
doApiRefresh = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.retry(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangahub
|
package eu.kanade.tachiyomi.multisrc.mangahub
|
||||||
|
|
||||||
class GraphQLTag
|
class GraphQLTag(
|
||||||
|
val refreshUrl: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
val searchQuery = { mangaSource: String, query: String, genre: String, order: String, page: Int ->
|
val searchQuery = { mangaSource: String, query: String, genre: String, order: String, page: Int ->
|
||||||
"""
|
"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user