Fix manhuaren no value for response (#17061) (#17134)

This commit is contained in:
felixfon 2023-07-16 23:55:40 +08:00 committed by GitHub
parent be155c280f
commit bbe45afa60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 227 additions and 55 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'Manhuaren'
pkgNameSuffix = 'zh.manhuaren'
extClass = '.Manhuaren'
extVersionCode = 11
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.zh.manhuaren
import android.text.format.DateFormat
import android.util.Base64
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -12,18 +13,26 @@ import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.Buffer
import org.json.JSONArray
import org.json.JSONObject
import java.net.URLEncoder
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.spec.X509EncodedKeySpec
import java.text.SimpleDateFormat
import java.util.ArrayList
import java.util.Date
import java.util.Locale
import java.util.UUID
import java.util.concurrent.TimeUnit.MINUTES
import javax.crypto.Cipher
import kotlin.random.Random
import kotlin.random.nextUBytes
class Manhuaren : HttpSource() {
override val lang = "zh"
@ -34,45 +43,188 @@ class Manhuaren : HttpSource() {
private val pageSize = 20
private val baseHttpUrl = baseUrl.toHttpUrlOrNull()!!
private val c = "4e0a48e1c0b54041bce9c8f0e036124d"
private val cacheControl: CacheControl by lazy { CacheControl.Builder().maxAge(10, MINUTES).build() }
private val userId = (100000000..4294967295).random().toString()
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 fun generateGSNHash(url: HttpUrl): String {
var s = c + "GET"
url.queryParameterNames.toSortedSet().forEach {
if (it != "gsn") {
s += it
s += urlEncode(url.queryParameterValues(it)[0])
}
private fun randomNumber(length: Int): String {
var str = ""
for (i in 1..length) {
str += (0..9).random().toString()
}
s += c
return hashString("MD5", s)
return str
}
private fun myGet(url: HttpUrl): Request {
private fun addLuhnCheckDigit(str: String): String {
var sum = 0
str.toCharArray().forEachIndexed { i, it ->
var v = Character.getNumericValue(it)
sum += if (i % 2 == 0) {
v
} else {
v *= 2
if (v < 10) {
v
} else {
v - 9
}
}
}
var checkDigit = sum % 10
if (checkDigit != 0) {
checkDigit = 10 - checkDigit
}
return "$str$checkDigit"
}
private fun generateIMEI(): String {
return addLuhnCheckDigit(randomNumber(14))
}
private fun generateSimSerialNumber(): String {
return addLuhnCheckDigit("891253${randomNumber(12)}")
}
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")
userId = response.getString("userId")
lastUsedTime = generateLastUsedTime()
return "$scheme $parameter"
}
private fun generateLastUsedTime(): String {
return ((Date().time / 1000).toInt() * 1000).toString()
}
private fun encrypt(message: String): String {
val x509EncodedKeySpec = X509EncodedKeySpec(Base64.decode(encodedPublicKey, Base64.DEFAULT))
val publicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec)
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
return Base64.encodeToString(cipher.doFinal(message.toByteArray()), Base64.NO_WRAP)
}
@OptIn(ExperimentalUnsignedTypes::class)
private fun getAnonyUser(): Request {
val url = baseHttpUrl.newBuilder()
.addPathSegments("v1/user/createAnonyUser2")
.build()
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<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(
HashMap<String, Any?>().apply {
put("key", encrypt(androidId)) // https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID
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(
HashMap<String, Any?>().apply {
put("key", encrypt(UUID.randomUUID().toString()))
put("keyType", "-1")
},
)
}
val bodyMap = HashMap<String, Any?>().apply {
put("keys", keysMap)
}
return myPost(
url,
JSONObject(bodyMap).toString()
.replaceFirst("^/+".toRegex(), "")
.toRequestBody("application/json".toMediaTypeOrNull()),
)
}
private fun addGsnHash(request: Request): Request {
val isPost = request.method == "POST"
val params = request.url.queryParameterNames.toMutableSet()
val bodyBuffer = Buffer()
if (isPost) {
params.add("body")
request.body?.writeTo(bodyBuffer)
}
var str = gsnSalt + request.method
params.toSortedSet().forEach {
if (it != "gsn") {
val value = if (isPost && it == "body") bodyBuffer.readUtf8() else request.url.queryParameter(it)
str += "$it${urlEncode(value)}"
}
}
str += gsnSalt
val gsn = hashString("MD5", str)
val newUrl = request.url.newBuilder()
.addQueryParameter("gsn", gsn)
.build()
return request.newBuilder()
.url(newUrl)
.build()
}
private fun myRequest(url: HttpUrl, method: String, body: RequestBody?): Request {
val now = DateFormat.format("yyyy-MM-dd+HH:mm:ss", Date()).toString()
val realUrl = url.newBuilder()
val newUrl = url.newBuilder()
.setQueryParameter("gsm", "md5")
.setQueryParameter("gft", "json")
.setQueryParameter("gak", "android_manhuaren2")
.setQueryParameter("gat", "")
.setQueryParameter("gui", userId)
.setQueryParameter("gts", now) // timestamp yyyy-MM-dd+HH:mm:ss
.setQueryParameter("gts", now)
.setQueryParameter("gut", "0") // user type
.setQueryParameter("gem", "1")
.setQueryParameter("gaui", "1")
.setQueryParameter("gaui", userId)
.setQueryParameter("gln", "") // location
.setQueryParameter("gcy", "US") // country
.setQueryParameter("gle", "zh") // language
.setQueryParameter("gcl", "dm5") // umeng channel
.setQueryParameter("gcl", "dm5") // Umeng channel
.setQueryParameter("gos", "1") // OS (int)
.setQueryParameter("gov", "22_5.1.1") // "{Build.VERSION.SDK_INT}_{Build.VERSION.RELEASE}"
.setQueryParameter("gav", "7.0.1") // app version
.setQueryParameter("gdi", "358240051111110") // device info
.setQueryParameter("gfcl", "dm5") // umeng channel config
.setQueryParameter("gfut", "1688140800000") // first used time
.setQueryParameter("glut", "1688140800000") // last used time
.setQueryParameter("gdi", imei)
.setQueryParameter("gfcl", "dm5") // Umeng channel config
.setQueryParameter("gfut", lastUsedTime) // first used time
.setQueryParameter("glut", lastUsedTime) // last used time
.setQueryParameter("gpt", "com.mhr.mangamini") // package name
.setQueryParameter("gciso", "us") // https://developer.android.com/reference/android/telephony/TelephonyManager#getSimCountryIso()
.setQueryParameter("glot", "") // longitude
@ -87,11 +239,28 @@ class Manhuaren : HttpSource() {
.setQueryParameter("glcn", "") // country name
.setQueryParameter("glcc", "") // country code
.setQueryParameter("gflcc", "") // first location country code
.build()
return Request.Builder()
.url(realUrl.setQueryParameter("gsn", generateGSNHash(realUrl.build())).build())
.headers(headers)
.cacheControl(cacheControl)
return addGsnHash(
Request.Builder()
.method(method, body)
.url(newUrl)
.headers(headers)
.build(),
)
}
private fun myPost(url: HttpUrl, body: RequestBody?): Request {
return myRequest(url, "POST", body).newBuilder()
.cacheControl(CacheControl.Builder().noCache().noStore().build())
.build()
}
private fun myGet(url: HttpUrl): Request {
val authorization = token
return myRequest(url, "GET", null).newBuilder()
.addHeader("Authorization", authorization)
.cacheControl(CacheControl.Builder().maxAge(10, MINUTES).build())
.build()
}
@ -100,27 +269,27 @@ class Manhuaren : HttpSource() {
put("at", -1)
put("av", "7.0.1") // app version
put("ciso", "us") // https://developer.android.com/reference/android/telephony/TelephonyManager#getSimCountryIso()
put("cl", "dm5") // umeng channel
put("cl", "dm5") // Umeng channel
put("cy", "US") // country
put("di", "358240051111110") // device info
put("dm", "Android SDK built for x86") // Build.MODEL
put("fcl", "dm5") // umeng channel config
put("di", imei)
put("dm", "Pixel 6") // https://developer.android.com/reference/android/os/Build#MODEL
put("fcl", "dm5") // Umeng channel config
put("ft", "mhr") // from type
put("fut", "1688140800000") // first used time
put("fut", lastUsedTime) // first used time
put("installation", "dm5")
put("le", "zh") // language
put("ln", "") // location
put("lut", "1688140800000") // last used time
put("lut", lastUsedTime) // last used time
put("nt", 4)
put("os", 1) // OS (int)
put("ov", "22_5.1.1") // "{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("rn", "1440x2952") // screen "{width}x{height}"
put("rn", "1080x2400") // screen "{width}x{height}"
put("st", 0)
}
val yqppMap = HashMap<String, Any?>().apply {
put("ciso", "us") // https://developer.android.com/reference/android/telephony/TelephonyManager#getSimCountryIso()
put("laut", "0") // is allow location (0 or 1)
put("laut", "0") // is allow location ("0" or "1")
put("lot", "") // longitude
put("lat", "") // latitude
put("cut", "GMT+8") // time zone
@ -140,7 +309,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", "Mozilla/5.0 (Linux; Android 5.1.1; Android SDK built for x86 Build/LMY48X) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/39.0.0.0 Mobile Safari/537.36")
add("User-Agent", "Mozilla/5.0 (Linux; Android 13; Pixel 6 Build/TQ2A.230505.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/115.0.5790.21 Mobile Safari/537.36")
}
}
@ -161,7 +330,7 @@ class Manhuaren : HttpSource() {
}
private fun urlEncode(str: String?): String {
return URLEncoder.encode(str, "UTF-8")
return URLEncoder.encode(str ?: "", "UTF-8")
.replace("+", "%20")
.replace("%7E", "~")
.replace("*", "%2A")
@ -202,7 +371,7 @@ class Manhuaren : HttpSource() {
.addQueryParameter("start", (pageSize * (page - 1)).toString())
.addQueryParameter("limit", pageSize.toString())
.addQueryParameter("sort", "0")
.addPathSegments("/v2/manga/getCategoryMangas")
.addPathSegments("v2/manga/getCategoryMangas")
.build()
return myGet(url)
}
@ -214,7 +383,7 @@ class Manhuaren : HttpSource() {
.addQueryParameter("start", (pageSize * (page - 1)).toString())
.addQueryParameter("limit", pageSize.toString())
.addQueryParameter("sort", "1")
.addPathSegments("/v2/manga/getCategoryMangas")
.addPathSegments("v2/manga/getCategoryMangas")
.build()
return myGet(url)
}
@ -233,30 +402,33 @@ class Manhuaren : HttpSource() {
.addQueryParameter("limit", pageSize.toString())
if (query != "") {
url = url.addQueryParameter("keywords", query)
.addPathSegments("/v1/search/getSearchManga")
return myGet(url.build())
}
filters.forEach { filter ->
when (filter) {
is SortFilter -> url = url.setQueryParameter("sort", filter.getId())
is CategoryFilter -> {
url = url.setQueryParameter("subCategoryId", filter.getId())
.setQueryParameter("subCategoryType", filter.getType())
.addPathSegments("v1/search/getSearchManga")
} else {
filters.forEach { filter ->
when (filter) {
is SortFilter -> {
url = url.setQueryParameter("sort", filter.getId())
}
is CategoryFilter -> {
url = url.setQueryParameter("subCategoryId", filter.getId())
.setQueryParameter("subCategoryType", filter.getType())
}
else -> {}
}
else -> {}
}
url = url.addPathSegments("v2/manga/getCategoryMangas")
}
url = url.addPathSegments("/v2/manga/getCategoryMangas")
return myGet(url.build())
}
override fun searchMangaParse(response: Response): MangasPage {
val res = response.body.string()
val obj = JSONObject(res).getJSONObject("response")
if (obj.has("result")) {
return mangasFromJSONArray(obj.getJSONArray("result"))
}
return mangasFromJSONArray(obj.getJSONArray("mangas"))
return mangasFromJSONArray(
obj.getJSONArray(
if (obj.has("result")) "result" else "mangas",
),
)
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {