mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-25 19:52:56 +01:00
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
036ef6580c
commit
f395eade8d
1
src/id/mangaku/assets/crypto-js.min.js
vendored
Normal file
1
src/id/mangaku/assets/crypto-js.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
src/id/mangaku/assets/zepto.min.js
vendored
Normal file
2
src/id/mangaku/assets/zepto.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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'))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user