Fix manhuaren No value for response (#701)
fix(manhuaren): store the token to avoid being blocked
This commit is contained in:
parent
eb364ada84
commit
72283a476a
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Manhuaren'
|
extName = 'Manhuaren'
|
||||||
extClass = '.Manhuaren'
|
extClass = '.Manhuaren'
|
||||||
extVersionCode = 14
|
extVersionCode = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,13 @@
|
||||||
package eu.kanade.tachiyomi.extension.zh.manhuaren
|
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.text.format.DateFormat
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
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.CacheControl
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
@ -22,6 +31,8 @@ import okhttp3.Response
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.security.KeyFactory
|
import java.security.KeyFactory
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
@ -35,7 +46,7 @@ import javax.crypto.Cipher
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.random.nextUBytes
|
import kotlin.random.nextUBytes
|
||||||
|
|
||||||
class Manhuaren : HttpSource() {
|
class Manhuaren : HttpSource(), ConfigurableSource {
|
||||||
override val lang = "zh"
|
override val lang = "zh"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
override val name = "漫画人"
|
override val name = "漫画人"
|
||||||
|
@ -43,20 +54,30 @@ class Manhuaren : HttpSource() {
|
||||||
|
|
||||||
private val pageSize = 20
|
private val pageSize = 20
|
||||||
private val baseHttpUrl = baseUrl.toHttpUrl()
|
private val baseHttpUrl = baseUrl.toHttpUrl()
|
||||||
|
private val preferences: SharedPreferences =
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
|
||||||
private val gsnSalt = "4e0a48e1c0b54041bce9c8f0e036124d"
|
private val gsnSalt = "4e0a48e1c0b54041bce9c8f0e036124d"
|
||||||
private val encodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmFCg289dTws27v8GtqIffkP4zgFR+MYIuUIeVO5AGiBV0rfpRh5gg7i8RrT12E9j6XwKoe3xJz1khDnPc65P5f7CJcNJ9A8bj7Al5K4jYGxz+4Q+n0YzSllXPit/Vz/iW5jFdlP6CTIgUVwvIoGEL2sS4cqqqSpCDKHSeiXh9CtMsktc6YyrSN+8mQbBvoSSew18r/vC07iQiaYkClcs7jIPq9tuilL//2uR9kWn5jsp8zHKVjmXuLtHDhM9lObZGCVJwdlN2KDKTh276u/pzQ1s5u8z/ARtK26N8e5w8mNlGcHcHfwyhjfEQurvrnkqYH37+12U3jGk5YNHGyOPcwIDAQAB"
|
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 imei: String by lazy { generateIMEI() }
|
||||||
private val token: String by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { fetchToken() }
|
private val token: String by lazy { fetchToken() }
|
||||||
private var userId = "-1"
|
private var userId: String = preferences.getString(USER_ID_PREF, null) ?: "-1"
|
||||||
private var lastUsedTime = ""
|
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 {
|
private fun randomNumber(length: Int): String {
|
||||||
var str = ""
|
return randomString(length, "0123456789")
|
||||||
for (i in 1..length) {
|
|
||||||
str += (0..9).random().toString()
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addLuhnCheckDigit(str: String): String {
|
private fun addLuhnCheckDigit(str: String): String {
|
||||||
|
@ -86,21 +107,47 @@ class Manhuaren : HttpSource() {
|
||||||
return addLuhnCheckDigit(randomNumber(14))
|
return addLuhnCheckDigit(randomNumber(14))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateSimSerialNumber(): String {
|
// private fun generateSimSerialNumber(): String {
|
||||||
return addLuhnCheckDigit("891253${randomNumber(12)}")
|
// 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 {
|
private fun fetchToken(): String {
|
||||||
|
var token = preferences.getString(TOKEN_PREF, null)
|
||||||
|
if (token == null || userId == "-1") {
|
||||||
val res = client.newCall(getAnonyUser()).execute()
|
val res = client.newCall(getAnonyUser()).execute()
|
||||||
val body = JSONObject(res.body.string())
|
val tokenResponse = Json.decodeFromString<GetAnonyUserBody>(res.body.string()).response
|
||||||
val response = body.getJSONObject("response")
|
val tokenResult = tokenResponse.tokenResult
|
||||||
val tokenResult = response.getJSONObject("tokenResult")
|
|
||||||
val scheme = tokenResult.getString("scheme")
|
|
||||||
val parameter = tokenResult.getString("parameter")
|
|
||||||
|
|
||||||
userId = response.getString("userId")
|
token = "${tokenResult.scheme} ${tokenResult.parameter}"
|
||||||
lastUsedTime = generateLastUsedTime()
|
userId = tokenResponse.userId.toString()
|
||||||
return "$scheme $parameter"
|
|
||||||
|
preferences.edit().apply {
|
||||||
|
putString(TOKEN_PREF, token)
|
||||||
|
putString(USER_ID_PREF, userId)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateLastUsedTime(): String {
|
private fun generateLastUsedTime(): String {
|
||||||
|
@ -122,21 +169,39 @@ class Manhuaren : HttpSource() {
|
||||||
.addPathSegments("v1/user/createAnonyUser2")
|
.addPathSegments("v1/user/createAnonyUser2")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val simSerialNumber = generateSimSerialNumber()
|
// val simSerialNumber = generateSimSerialNumber()
|
||||||
val mac = Random.nextUBytes(6)
|
// val mac = Random.nextUBytes(6)
|
||||||
.joinToString(":") { it.toString(16).padStart(2, '0') }
|
// .joinToString(":") { it.toString(16).padStart(2, '0') }
|
||||||
val androidId = Random.nextUBytes(8)
|
val androidId = Random.nextUBytes(8)
|
||||||
.joinToString("") { it.toString(16).padStart(2, '0') }
|
.joinToString("") { it.toString(16).padStart(2, '0') }
|
||||||
.replaceFirst("^0+".toRegex(), "")
|
.replaceFirst("^0+".toRegex(), "")
|
||||||
.uppercase()
|
.uppercase()
|
||||||
|
|
||||||
val keysMap = ArrayList<HashMap<String, Any?>>().apply {
|
val keysMap = ArrayList<HashMap<String, Any?>>().apply {
|
||||||
|
add(
|
||||||
|
HashMap<String, Any?>().apply {
|
||||||
|
put("key", encrypt(imei))
|
||||||
|
put("keyType", "0")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// add(
|
||||||
|
// HashMap<String, Any?>().apply {
|
||||||
|
// put("key", encrypt("mac: $mac"))
|
||||||
|
// put("keyType", "1")
|
||||||
|
// },
|
||||||
|
// )
|
||||||
add(
|
add(
|
||||||
HashMap<String, Any?>().apply {
|
HashMap<String, Any?>().apply {
|
||||||
put("key", encrypt(androidId)) // https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID
|
put("key", encrypt(androidId)) // https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID
|
||||||
put("keyType", "2")
|
put("keyType", "2")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
// add(
|
||||||
|
// HashMap<String, Any?>().apply {
|
||||||
|
// put("key", encrypt(simSerialNumber)) // https://developer.android.com/reference/android/telephony/TelephonyManager#getSimSerialNumber()
|
||||||
|
// put("keyType", "3")
|
||||||
|
// },
|
||||||
|
// )
|
||||||
add(
|
add(
|
||||||
HashMap<String, Any?>().apply {
|
HashMap<String, Any?>().apply {
|
||||||
put("key", encrypt(UUID.randomUUID().toString()))
|
put("key", encrypt(UUID.randomUUID().toString()))
|
||||||
|
@ -255,7 +320,7 @@ class Manhuaren : HttpSource() {
|
||||||
put("cl", "dm5") // Umeng channel
|
put("cl", "dm5") // Umeng channel
|
||||||
put("cy", "US") // country
|
put("cy", "US") // country
|
||||||
put("di", imei)
|
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("fcl", "dm5") // Umeng channel config
|
||||||
put("ft", "mhr") // from type
|
put("ft", "mhr") // from type
|
||||||
put("fut", lastUsedTime) // first used time
|
put("fut", lastUsedTime) // first used time
|
||||||
|
@ -267,7 +332,7 @@ class Manhuaren : HttpSource() {
|
||||||
put("os", 1) // OS (int)
|
put("os", 1) // OS (int)
|
||||||
put("ov", "33_13") // "{Build.VERSION.SDK_INT}_{Build.VERSION.RELEASE}"
|
put("ov", "33_13") // "{Build.VERSION.SDK_INT}_{Build.VERSION.RELEASE}"
|
||||||
put("pt", "com.mhr.mangamini") // package name
|
put("pt", "com.mhr.mangamini") // package name
|
||||||
put("rn", "1400x3120") // screen "{width}x{height}"
|
put("rn", "1080x1920") // screen "{width}x{height}"
|
||||||
put("st", 0)
|
put("st", 0)
|
||||||
}
|
}
|
||||||
val yqppMap = HashMap<String, Any?>().apply {
|
val yqppMap = HashMap<String, Any?>().apply {
|
||||||
|
@ -292,7 +357,7 @@ class Manhuaren : HttpSource() {
|
||||||
add("yq_is_anonymous", "1")
|
add("yq_is_anonymous", "1")
|
||||||
add("x-request-id", UUID.randomUUID().toString())
|
add("x-request-id", UUID.randomUUID().toString())
|
||||||
add("X-Yq-Yqpp", JSONObject(yqppMap).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 getId() = vals[state].id
|
||||||
fun getType() = vals[state].type
|
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"
|
||||||
|
|
|
@ -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 ?: ""
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue