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:
parent
b6e923ac49
commit
8a55ca5d6f
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -2,12 +2,7 @@ ext {
|
||||||
extName = 'Mangaku'
|
extName = 'Mangaku'
|
||||||
pkgNameSuffix = 'id.mangaku'
|
pkgNameSuffix = 'id.mangaku'
|
||||||
extClass = '.Mangaku'
|
extClass = '.Mangaku'
|
||||||
extVersionCode = 7
|
extVersionCode = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(':lib-cryptoaes'))
|
|
||||||
implementation(project(':lib-unpacker'))
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package eu.kanade.tachiyomi.extension.id.mangaku
|
package eu.kanade.tachiyomi.extension.id.mangaku
|
||||||
|
|
||||||
import android.net.Uri
|
import android.annotation.SuppressLint
|
||||||
import android.util.Base64
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.os.Handler
|
||||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
import android.os.Looper
|
||||||
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
|
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.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
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.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
@ -23,7 +29,11 @@ import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.select.Elements
|
import org.jsoup.select.Elements
|
||||||
import rx.Observable
|
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() {
|
class Mangaku : ParsedHttpSource() {
|
||||||
|
|
||||||
|
@ -111,18 +121,24 @@ class Mangaku : ParsedHttpSource() {
|
||||||
override fun searchMangaNextPageSelector(): String? = null
|
override fun searchMangaNextPageSelector(): String? = null
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||||
title = document.select(".post.singlep .titles a").text().replace("Bahasa Indonesia", "").trim()
|
title = document
|
||||||
thumbnail_url = document.select(".post.singlep img").attr("abs:src")
|
.select("h1.titles a, h1.title").text()
|
||||||
document.select("#wrapper-a #content-a .inf").forEach {
|
.replace("Bahasa Indonesia", "").trim()
|
||||||
when (it.select(".infx").text()) {
|
|
||||||
"Genre" -> genre = it.select("p a[rel=tag]").joinToString { it.text() }
|
thumbnail_url = document
|
||||||
"Author" -> author = it.select("p").text()
|
.select("#sidebar-a a[imageanchor] > img, #abc a[imageanchor] > img")
|
||||||
"Sinopsis" -> description = it.select("p").text()
|
.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 {
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
@ -135,164 +151,97 @@ class Mangaku : ParsedHttpSource() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
val wpRoutineUrl = document.selectFirst("script[src*=wp-routine]")!!.attr("abs:src")
|
val interfaceName = randomString()
|
||||||
Log.d("mangaku", "wp-routine: $wpRoutineUrl")
|
|
||||||
|
|
||||||
val wpRoutineJs = client.newCall(GET(wpRoutineUrl, headers)).execute().use {
|
val decodeScriptOriginal = document
|
||||||
it.body.string()
|
.select("script:containsData(dtx = )")
|
||||||
|
.joinToString("\n") { it.data() }
|
||||||
|
val decodeScript = decodeScriptOriginal.replace(urlsnxRe) {
|
||||||
|
it.value + "window.$interfaceName.passPayload(JSON.stringify(urlsnx));"
|
||||||
}
|
}
|
||||||
|
|
||||||
val upt3 = wpRoutineJs
|
val wpRoutineUrl = document
|
||||||
.substringAfterLast("upt3(")
|
.selectFirst("script[src*=wp-routine]")!!
|
||||||
.substringBefore(");")
|
.attr("abs:src")
|
||||||
val keymapJsPacked = wpRoutineJs
|
val wpRoutineScript = client
|
||||||
.substringAfter("eval(function(x,a,c,k,e,d)")
|
.newCall(GET(wpRoutineUrl, headers))
|
||||||
.substringBefore(".split('|'),0,{}))") + ".split('|'),0,{}))"
|
.execute().use { it.body.string() }
|
||||||
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 dtxScript = document.selectFirst("script:containsData(var dtx =)")!!.html()
|
val handler = Handler(Looper.getMainLooper())
|
||||||
val dtxIsEqualTo = dtxScript
|
val latch = CountDownLatch(1)
|
||||||
.substringAfter("var dtx = ")
|
val jsInterface = JsInterface(latch)
|
||||||
.substringBefore(";")
|
var webView: WebView? = null
|
||||||
val dtx = dtxScript
|
|
||||||
.substringAfter("var $dtxIsEqualTo= \"")
|
|
||||||
.substringBefore("\"")
|
|
||||||
|
|
||||||
val mainScriptTag = document.selectFirst("script:containsData(await jrsx)")!!.html()
|
handler.post {
|
||||||
val jrsxArgs = mainScriptTag
|
val webview = WebView(Injekt.get<Application>())
|
||||||
.substringAfter("await jrsx(")
|
webView = webview
|
||||||
.substringBefore(");")
|
webview.settings.javaScriptEnabled = true
|
||||||
.split(",")
|
webview.settings.blockNetworkLoads = true
|
||||||
Log.d("mangaku", "args: $jrsxArgs")
|
webview.settings.blockNetworkImage = true
|
||||||
|
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||||
|
webview.addJavascriptInterface(jsInterface, interfaceName)
|
||||||
|
|
||||||
val thirdArgValue = mainScriptTag
|
webview.webViewClient = object : WebViewClient() {
|
||||||
.substringAfter("const ${jrsxArgs[2]} = '")
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
.substringBefore("'")
|
view.evaluateJavascript(jQueryScript) {}
|
||||||
Log.d("mangaku", "arg2: $thirdArgValue")
|
view.evaluateJavascript(cryptoJSScript) {}
|
||||||
|
view.evaluateJavascript(wpRoutineScript) {}
|
||||||
val encodedAttr = jrsxArgs[4].removeSurrounding("'")
|
view.evaluateJavascript(decodeScript) {}
|
||||||
|
}
|
||||||
val upt4arg = mainScriptTag
|
}
|
||||||
.substringAfter("const ${jrsxArgs[3]} = await upt4('")
|
webview.loadDataWithBaseURL(
|
||||||
.substringBefore("'")
|
document.location(),
|
||||||
Log.d("mangaku", "upt4arg: $upt4arg")
|
"",
|
||||||
val upt4value = upt4(appMgk, upt4arg)
|
"text/html",
|
||||||
|
"UTF-8",
|
||||||
val decrypted = CryptoAES.decrypt(dtx, huzorttshj(thirdArgValue, upt4value))
|
null,
|
||||||
.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,
|
|
||||||
)
|
)
|
||||||
.toString(Charsets.UTF_8)
|
}
|
||||||
.replace("+", "%20")
|
|
||||||
.percentDecode()
|
// 5s is ten times over the execution time on a crappy emulator
|
||||||
Page(idx, imageUrl = url)
|
latch.await(5, TimeUnit.SECONDS)
|
||||||
}.toList()
|
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()
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
private val rsxxxRe = Regex(""".............?\+.......""")
|
private val urlsnxRe = Regex("""urlsnx=(?!\[];)[^;]+;""")
|
||||||
|
|
||||||
private val noLetterRe = Regex("""[^a-z]""")
|
private fun randomString(length: Int = 10): String {
|
||||||
|
val charPool = ('a'..'z') + ('A'..'Z')
|
||||||
private val noNumberRe = Regex("""[^0-9]""")
|
return List(length) { charPool.random() }.joinToString("")
|
||||||
|
|
||||||
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 upt4(appMgk: String, key: String): String {
|
internal class JsInterface(private val latch: CountDownLatch) {
|
||||||
val fullKey = key + appMgk.map {
|
|
||||||
(it.code xor 71).toChar()
|
|
||||||
}.joinToString("")
|
|
||||||
|
|
||||||
val b64FullKey = btoa(fullKey)
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
val sixLastChars = b64FullKey.substring(b64FullKey.length - 7, b64FullKey.length - 1)
|
var images: List<String> = listOf()
|
||||||
val elevenFirstChars = b64FullKey.substring(0, 12)
|
private set
|
||||||
val keyFragment = btoa(sixLastChars + elevenFirstChars).trim()
|
|
||||||
val firstDigest = digest("SHA384", keyFragment)
|
|
||||||
|
|
||||||
val uniqueLetters = firstDigest.replace(noLetterRe, "").distinct()
|
@JavascriptInterface
|
||||||
val uniqueNumbers = firstDigest.replace(noNumberRe, "").distinct()
|
fun passPayload(rawData: String) {
|
||||||
val joined = uniqueNumbers + uniqueLetters
|
val data = json.parseToJsonElement(rawData).jsonArray
|
||||||
|
images = data.map { it.jsonPrimitive.content }
|
||||||
val secondDigest = digest("SHA1", joined)
|
latch.countDown()
|
||||||
val secondDigestReversed = secondDigest.reversed()
|
}
|
||||||
|
|
||||||
val rotated = stringRotator(secondDigestReversed) + "-$key"
|
|
||||||
return keyFragment + joined + secondDigestReversed + rotated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stringRotator(
|
private val jQueryScript = javaClass
|
||||||
input: String,
|
.getResource("/assets/zepto.min.js")!!
|
||||||
multiplier: Int = 73,
|
.readText() // Zepto v1.2.0 (jQuery compatible)
|
||||||
adder: Int = 93,
|
private val cryptoJSScript = javaClass
|
||||||
length: Int = 20,
|
.getResource("/assets/crypto-js.min.js")!!
|
||||||
strings: List<String> = listOf("PEWAW", "MJKJG", "SGWRT", "KUIQ"),
|
.readText() // CryptoJS v4.0.0 (on site: cpr2.js)
|
||||||
): 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()
|
|
||||||
}
|
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue