From 72283a476a0f0b5b282ad13be8b17b652ed43dde Mon Sep 17 00:00:00 2001 From: felixfon <66350602+felixfon@users.noreply.github.com> Date: Sun, 28 Jan 2024 03:00:02 +0800 Subject: [PATCH] Fix manhuaren No value for response (#701) fix(manhuaren): store the token to avoid being blocked --- src/zh/manhuaren/build.gradle | 2 +- .../zh/manhuaren/ErrorResponseInterceptor.kt | 31 ++++ .../extension/zh/manhuaren/Manhuaren.kt | 133 ++++++++++++++---- .../zh/manhuaren/SimpleEditTextPreference.kt | 10 ++ 4 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/ErrorResponseInterceptor.kt create mode 100644 src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/SimpleEditTextPreference.kt diff --git a/src/zh/manhuaren/build.gradle b/src/zh/manhuaren/build.gradle index cc6311d5e..0c675383a 100644 --- a/src/zh/manhuaren/build.gradle +++ b/src/zh/manhuaren/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Manhuaren' extClass = '.Manhuaren' - extVersionCode = 14 + extVersionCode = 15 } apply from: "$rootDir/common.gradle" diff --git a/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/ErrorResponseInterceptor.kt b/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/ErrorResponseInterceptor.kt new file mode 100644 index 000000000..62b8b31fd --- /dev/null +++ b/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/ErrorResponseInterceptor.kt @@ -0,0 +1,31 @@ +package eu.kanade.tachiyomi.extension.zh.manhuaren + +import android.content.SharedPreferences +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import java.io.IOException + +class ErrorResponseInterceptor(private val baseUrl: String, private val preferences: SharedPreferences) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + if (!request.url.toString().startsWith(baseUrl)) return response + + val body = response.body + val content = body.string() + if ("errorResponse" in Json.parseToJsonElement(content).jsonObject) { + preferences.edit() + .remove(TOKEN_PREF) + .remove(USER_ID_PREF) + .apply() + throw IOException("用户ID已自动清除,请再試一次") + } + + return response.newBuilder() + .body(content.toResponseBody(body.contentType())) + .build() + } +} diff --git a/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/Manhuaren.kt b/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/Manhuaren.kt index cfb9df130..555dbda1c 100644 --- a/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/Manhuaren.kt +++ b/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/Manhuaren.kt @@ -1,8 +1,13 @@ package eu.kanade.tachiyomi.extension.zh.manhuaren +import android.app.Application +import android.content.SharedPreferences +import android.os.Build import android.text.format.DateFormat import android.util.Base64 +import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -10,11 +15,15 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import okhttp3.CacheControl import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody @@ -22,6 +31,8 @@ import okhttp3.Response import okio.Buffer import org.json.JSONArray import org.json.JSONObject +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.net.URLEncoder import java.security.KeyFactory import java.security.MessageDigest @@ -35,7 +46,7 @@ import javax.crypto.Cipher import kotlin.random.Random import kotlin.random.nextUBytes -class Manhuaren : HttpSource() { +class Manhuaren : HttpSource(), ConfigurableSource { override val lang = "zh" override val supportsLatest = true override val name = "漫画人" @@ -43,20 +54,30 @@ class Manhuaren : HttpSource() { private val pageSize = 20 private val baseHttpUrl = baseUrl.toHttpUrl() + private val preferences: SharedPreferences = + Injekt.get().getSharedPreferences("source_$id", 0x0000) private val gsnSalt = "4e0a48e1c0b54041bce9c8f0e036124d" private val encodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmFCg289dTws27v8GtqIffkP4zgFR+MYIuUIeVO5AGiBV0rfpRh5gg7i8RrT12E9j6XwKoe3xJz1khDnPc65P5f7CJcNJ9A8bj7Al5K4jYGxz+4Q+n0YzSllXPit/Vz/iW5jFdlP6CTIgUVwvIoGEL2sS4cqqqSpCDKHSeiXh9CtMsktc6YyrSN+8mQbBvoSSew18r/vC07iQiaYkClcs7jIPq9tuilL//2uR9kWn5jsp8zHKVjmXuLtHDhM9lObZGCVJwdlN2KDKTh276u/pzQ1s5u8z/ARtK26N8e5w8mNlGcHcHfwyhjfEQurvrnkqYH37+12U3jGk5YNHGyOPcwIDAQAB" - private val imei: String by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { generateIMEI() } - private val token: String by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { fetchToken() } - private var userId = "-1" - private var lastUsedTime = "" + private val imei: String by lazy { generateIMEI() } + private val token: String by lazy { fetchToken() } + private var userId: String = preferences.getString(USER_ID_PREF, null) ?: "-1" + private val lastUsedTime: String by lazy { generateLastUsedTime() } + + override val client: OkHttpClient = network.client + .newBuilder() + .apply { interceptors().removeAll { it.javaClass.simpleName == "BrotliInterceptor" } } + .addInterceptor(ErrorResponseInterceptor(baseUrl, preferences)) + .build() + + private fun randomString(length: Int, pool: String): String { + return (1..length) + .map { Random.nextInt(0, pool.length).let { pool[it] } } + .joinToString("") + } private fun randomNumber(length: Int): String { - var str = "" - for (i in 1..length) { - str += (0..9).random().toString() - } - return str + return randomString(length, "0123456789") } private fun addLuhnCheckDigit(str: String): String { @@ -86,21 +107,47 @@ class Manhuaren : HttpSource() { return addLuhnCheckDigit(randomNumber(14)) } - private fun generateSimSerialNumber(): String { - return addLuhnCheckDigit("891253${randomNumber(12)}") - } + // private fun generateSimSerialNumber(): String { + // return addLuhnCheckDigit("891253${randomNumber(12)}") + // } + + @Serializable + data class TokenResult( + val parameter: String, + val scheme: String, + ) + + @Serializable + data class TokenResponse( + val initDeviceKey: String, + val nickName: String, + val tokenResult: TokenResult, + val userId: Long, + val userName: String, + ) + + @Serializable + data class GetAnonyUserBody( + val response: TokenResponse, + ) private fun fetchToken(): String { - val res = client.newCall(getAnonyUser()).execute() - val body = JSONObject(res.body.string()) - val response = body.getJSONObject("response") - val tokenResult = response.getJSONObject("tokenResult") - val scheme = tokenResult.getString("scheme") - val parameter = tokenResult.getString("parameter") + var token = preferences.getString(TOKEN_PREF, null) + if (token == null || userId == "-1") { + val res = client.newCall(getAnonyUser()).execute() + val tokenResponse = Json.decodeFromString(res.body.string()).response + val tokenResult = tokenResponse.tokenResult - userId = response.getString("userId") - lastUsedTime = generateLastUsedTime() - return "$scheme $parameter" + token = "${tokenResult.scheme} ${tokenResult.parameter}" + userId = tokenResponse.userId.toString() + + preferences.edit().apply { + putString(TOKEN_PREF, token) + putString(USER_ID_PREF, userId) + }.apply() + } + + return token } private fun generateLastUsedTime(): String { @@ -122,21 +169,39 @@ class Manhuaren : HttpSource() { .addPathSegments("v1/user/createAnonyUser2") .build() - val simSerialNumber = generateSimSerialNumber() - val mac = Random.nextUBytes(6) - .joinToString(":") { it.toString(16).padStart(2, '0') } + // val simSerialNumber = generateSimSerialNumber() + // val mac = Random.nextUBytes(6) + // .joinToString(":") { it.toString(16).padStart(2, '0') } val androidId = Random.nextUBytes(8) .joinToString("") { it.toString(16).padStart(2, '0') } .replaceFirst("^0+".toRegex(), "") .uppercase() val keysMap = ArrayList>().apply { + add( + HashMap().apply { + put("key", encrypt(imei)) + put("keyType", "0") + }, + ) + // add( + // HashMap().apply { + // put("key", encrypt("mac: $mac")) + // put("keyType", "1") + // }, + // ) add( HashMap().apply { put("key", encrypt(androidId)) // https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID put("keyType", "2") }, ) + // add( + // HashMap().apply { + // put("key", encrypt(simSerialNumber)) // https://developer.android.com/reference/android/telephony/TelephonyManager#getSimSerialNumber() + // put("keyType", "3") + // }, + // ) add( HashMap().apply { put("key", encrypt(UUID.randomUUID().toString())) @@ -255,7 +320,7 @@ class Manhuaren : HttpSource() { put("cl", "dm5") // Umeng channel put("cy", "US") // country put("di", imei) - put("dm", "Pixel 6 Pro") // https://developer.android.com/reference/android/os/Build#MODEL + put("dm", Build.MODEL) put("fcl", "dm5") // Umeng channel config put("ft", "mhr") // from type put("fut", lastUsedTime) // first used time @@ -267,7 +332,7 @@ class Manhuaren : HttpSource() { put("os", 1) // OS (int) put("ov", "33_13") // "{Build.VERSION.SDK_INT}_{Build.VERSION.RELEASE}" put("pt", "com.mhr.mangamini") // package name - put("rn", "1400x3120") // screen "{width}x{height}" + put("rn", "1080x1920") // screen "{width}x{height}" put("st", 0) } val yqppMap = HashMap().apply { @@ -292,7 +357,7 @@ class Manhuaren : HttpSource() { add("yq_is_anonymous", "1") add("x-request-id", UUID.randomUUID().toString()) add("X-Yq-Yqpp", JSONObject(yqppMap).toString()) - add("User-Agent", "Dalvik/2.1.0 (Linux; U; Android 13; Pixel 6 Pro Build/TQ3A.230705.001)") + add("User-Agent", "Dalvik/2.1.0 (Linux; U; Android 13; ${Build.MODEL} Build/${Build.ID})") } } @@ -602,4 +667,16 @@ class Manhuaren : HttpSource() { fun getId() = vals[state].id fun getType() = vals[state].type } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SimpleEditTextPreference(screen.context).apply { + key = USER_ID_PREF + title = "用户ID" + + setEnabled(false) + }.let(screen::addPreference) + } } + +internal const val TOKEN_PREF = "token" +internal const val USER_ID_PREF = "userId" diff --git a/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/SimpleEditTextPreference.kt b/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/SimpleEditTextPreference.kt new file mode 100644 index 000000000..22735b84b --- /dev/null +++ b/src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/SimpleEditTextPreference.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.extension.zh.manhuaren + +import android.content.Context +import androidx.preference.EditTextPreference + +class SimpleEditTextPreference(context: Context?) : EditTextPreference(context) { + override fun getSummary(): CharSequence { + return text ?: "" + } +}