Mangaku: update selectors, fix URL decryption (#360)

* Mangaku: update selectors, fix URL decryption

* Translate error message to ID

Co-authored-by: Luqman <riyanluqman@gmail.com>

---------

Co-authored-by: Luqman <riyanluqman@gmail.com>
This commit is contained in:
Vetle Ledaal 2024-01-19 08:40:01 +00:00 committed by GitHub
parent 036ef6580c
commit f395eade8d
4 changed files with 106 additions and 159 deletions

File diff suppressed because one or more lines are too long

2
src/id/mangaku/assets/zepto.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,7 @@
ext {
extName = 'Mangaku'
extClass = '.Mangaku'
extVersionCode = 7
extVersionCode = 8
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:cryptoaes'))
implementation(project(':lib:unpacker'))
}

View File

@ -1,10 +1,13 @@
package eu.kanade.tachiyomi.extension.id.mangaku
import android.net.Uri
import android.util.Base64
import android.util.Log
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
@ -15,6 +18,9 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Request
@ -23,7 +29,11 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import rx.Observable
import java.security.MessageDigest
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class Mangaku : ParsedHttpSource() {
@ -111,18 +121,24 @@ class Mangaku : ParsedHttpSource() {
override fun searchMangaNextPageSelector(): String? = null
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.select(".post.singlep .titles a").text().replace("Bahasa Indonesia", "").trim()
thumbnail_url = document.select(".post.singlep img").attr("abs:src")
document.select("#wrapper-a #content-a .inf").forEach {
when (it.select(".infx").text()) {
"Genre" -> genre = it.select("p a[rel=tag]").joinToString { it.text() }
"Author" -> author = it.select("p").text()
"Sinopsis" -> description = it.select("p").text()
title = document
.select("h1.titles a, h1.title").text()
.replace("Bahasa Indonesia", "").trim()
thumbnail_url = document
.select("#sidebar-a a[imageanchor] > img, #abc a[imageanchor] > img")
.attr("abs:src")
document.select("#wrapper-a #content-a .inf, #abc .inf").forEach { row ->
when (row.select(".infx").text()) {
"Genre" -> genre = row.select("p a[rel=tag]").joinToString { it.text() }
"Author" -> author = row.select("p").text()
"Sinopsis" -> description = row.select("p").text()
}
}
}
override fun chapterListSelector() = "#content-b > div > a"
override fun chapterListSelector() = "#content-b > div > a, .fndsosmed-social + div > a"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
@ -135,164 +151,97 @@ class Mangaku : ParsedHttpSource() {
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun pageListParse(document: Document): List<Page> {
val wpRoutineUrl = document.selectFirst("script[src*=wp-routine]")!!.attr("abs:src")
Log.d("mangaku", "wp-routine: $wpRoutineUrl")
val interfaceName = randomString()
val wpRoutineJs = client.newCall(GET(wpRoutineUrl, headers)).execute().use {
it.body.string()
val decodeScriptOriginal = document
.select("script:containsData(dtx = )")
.joinToString("\n") { it.data() }
val decodeScript = decodeScriptOriginal.replace(urlsnxRe) {
it.value + "window.$interfaceName.passPayload(JSON.stringify(urlsnx));"
}
val upt3 = wpRoutineJs
.substringAfterLast("upt3(")
.substringBefore(");")
val keymapJsPacked = wpRoutineJs
.substringAfter("eval(function(x,a,c,k,e,d)")
.substringBefore(".split('|'),0,{}))") + ".split('|'),0,{}))"
val keymapJs = Unpacker.unpack(keymapJsPacked)
val appMgkVariable = keymapJs
.substringAfter("$upt3=")
.substringBefore(";")
val appMgk = keymapJs
.substringAfter("let $appMgkVariable=\"")
.substringBefore("\";")
.reversed()
Log.d("mangaku", "app-mgk: $appMgk")
val wpRoutineUrl = document
.selectFirst("script[src*=wp-routine]")!!
.attr("abs:src")
val wpRoutineScript = client
.newCall(GET(wpRoutineUrl, headers))
.execute().use { it.body.string() }
val dtxScript = document.selectFirst("script:containsData(var dtx =)")!!.html()
val dtxIsEqualTo = dtxScript
.substringAfter("var dtx = ")
.substringBefore(";")
val dtx = dtxScript
.substringAfter("var $dtxIsEqualTo= \"")
.substringBefore("\"")
val handler = Handler(Looper.getMainLooper())
val latch = CountDownLatch(1)
val jsInterface = JsInterface(latch)
var webView: WebView? = null
val mainScriptTag = document.selectFirst("script:containsData(await jrsx)")!!.html()
val jrsxArgs = mainScriptTag
.substringAfter("await jrsx(")
.substringBefore(");")
.split(",")
Log.d("mangaku", "args: $jrsxArgs")
handler.post {
val webview = WebView(Injekt.get<Application>())
webView = webview
webview.settings.javaScriptEnabled = true
webview.settings.blockNetworkLoads = true
webview.settings.blockNetworkImage = true
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
webview.addJavascriptInterface(jsInterface, interfaceName)
val thirdArgValue = mainScriptTag
.substringAfter("const ${jrsxArgs[2]} = '")
.substringBefore("'")
Log.d("mangaku", "arg2: $thirdArgValue")
val encodedAttr = jrsxArgs[4].removeSurrounding("'")
val upt4arg = mainScriptTag
.substringAfter("const ${jrsxArgs[3]} = await upt4('")
.substringBefore("'")
Log.d("mangaku", "upt4arg: $upt4arg")
val upt4value = upt4(appMgk, upt4arg)
val decrypted = CryptoAES.decrypt(dtx, huzorttshj(thirdArgValue, upt4value))
.replace(rsxxxRe, "")
.replace("_", "=")
.reversed()
.replace("+", "%20")
val htmImageList = Base64.decode(decrypted, Base64.DEFAULT)
.toString(Charsets.UTF_8)
.percentDecode()
val attr = stringRotator(encodedAttr, 23, 69, 9).lowercase()
val fifthArgValueDigest =
stringRotator(digest("SHA384", encodedAttr), 23, 69, 20).lowercase()
val re = Regex("""$attr=['"](.*?)['"]""")
return re.findAll(htmImageList).mapIndexed { idx, it ->
val url = Base64.decode(
CryptoAES.decrypt(it.groupValues[1], fifthArgValueDigest),
Base64.DEFAULT,
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
view.evaluateJavascript(jQueryScript) {}
view.evaluateJavascript(cryptoJSScript) {}
view.evaluateJavascript(wpRoutineScript) {}
view.evaluateJavascript(decodeScript) {}
}
}
webview.loadDataWithBaseURL(
document.location(),
"",
"text/html",
"UTF-8",
null,
)
.toString(Charsets.UTF_8)
.replace("+", "%20")
.percentDecode()
Page(idx, imageUrl = url)
}.toList()
}
// 5s is ten times over the execution time on a crappy emulator
latch.await(5, TimeUnit.SECONDS)
handler.post { webView?.destroy() }
if (latch.count == 1L) {
throw Exception("Kehabisan waktu saat men-decrypt tautan gambar") //Timeout while decrypting image links
}
return jsInterface.images.mapIndexed { i, url ->
Page(i, imageUrl = url)
}
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
private val rsxxxRe = Regex(""".............?\+.......""")
private val urlsnxRe = Regex("""urlsnx=(?!\[];)[^;]+;""")
private val noLetterRe = Regex("""[^a-z]""")
private val noNumberRe = Regex("""[^0-9]""")
private val whitespaceRe = Regex("""\s+""")
private fun huzorttshj(key: String, upt4val: String): String {
val mapping = "-ABCDEFGHIJKLMNOPQRSTUVWXYZ=0123456789abcdefghijklmnopqrstuvwxyz+"
val b64upt4 = btoa(upt4val).replace(whitespaceRe, "")
var idx = 0
return b64upt4.map {
val upt4idx = mapping.indexOf(it)
val keyidx = mapping.indexOf(key[idx])
val output = (mapping.substring(keyidx) + mapping.substring(0, keyidx))[upt4idx]
if (idx == key.length - 1) {
idx = 0
} else {
idx += 1
}
output
}.joinToString("")
private fun randomString(length: Int = 10): String {
val charPool = ('a'..'z') + ('A'..'Z')
return List(length) { charPool.random() }.joinToString("")
}
private fun upt4(appMgk: String, key: String): String {
val fullKey = key + appMgk.map {
(it.code xor 71).toChar()
}.joinToString("")
internal class JsInterface(private val latch: CountDownLatch) {
val b64FullKey = btoa(fullKey)
private val json: Json by injectLazy()
val sixLastChars = b64FullKey.substring(b64FullKey.length - 7, b64FullKey.length - 1)
val elevenFirstChars = b64FullKey.substring(0, 12)
val keyFragment = btoa(sixLastChars + elevenFirstChars).trim()
val firstDigest = digest("SHA384", keyFragment)
var images: List<String> = listOf()
private set
val uniqueLetters = firstDigest.replace(noLetterRe, "").distinct()
val uniqueNumbers = firstDigest.replace(noNumberRe, "").distinct()
val joined = uniqueNumbers + uniqueLetters
val secondDigest = digest("SHA1", joined)
val secondDigestReversed = secondDigest.reversed()
val rotated = stringRotator(secondDigestReversed) + "-$key"
return keyFragment + joined + secondDigestReversed + rotated
}
private fun stringRotator(
input: String,
multiplier: Int = 73,
adder: Int = 93,
length: Int = 20,
strings: List<String> = listOf("PEWAW", "MJKJG", "SGWRT", "KUIQ"),
): String {
val firstPass = input
.map { input.length * multiplier + (adder + it.code) }
.joinToString("") + adder.toString()
return firstPass.map {
val idx = it.toString().toInt()
if (idx < strings.size) strings[idx] else idx.toString()
@JavascriptInterface
fun passPayload(rawData: String) {
val data = json.parseToJsonElement(rawData).jsonArray
images = data.map { it.jsonPrimitive.content }
latch.countDown()
}
.joinToString("")
.padEnd(length)
.substring(0, length)
}
private fun btoa(input: String): String =
Base64.encode(input.toByteArray(), Base64.DEFAULT).toString(Charsets.UTF_8)
private fun digest(digest: String, input: String): String =
MessageDigest.getInstance(digest).digest(input.toByteArray())
.joinToString("") { "%02x".format(it) }
private fun String.distinct(): String = toCharArray().distinct().joinToString("")
private fun String.percentDecode(): String = Uri.decode(this)
private val jQueryScript = javaClass
.getResource("/assets/zepto.min.js")!!
.readText() // Zepto v1.2.0 (jQuery compatible)
private val cryptoJSScript = javaClass
.getResource("/assets/crypto-js.min.js")!!
.readText() // CryptoJS v4.0.0 (on site: cpr2.js)
}