From d8fef95969a6c92d90353121997ea63f6ed75c9b Mon Sep 17 00:00:00 2001
From: beerpsi <92439990+beerpiss@users.noreply.github.com>
Date: Sat, 14 Jan 2023 03:26:07 +0700
Subject: [PATCH] IU: descramble images (#14917)

* iu: descramble images

* bump overrideversioncode

* add comments

* merge iu_descramble and args

* docs

* make all Fs lowercase

* fix according to suggestions

* rename a bit
---
 .../immortalupdates/src/ImmortalUpdates.kt    | 194 ++++++++++++++++++
 .../multisrc/madara/MadaraGenerator.kt        |   2 +-
 2 files changed, 195 insertions(+), 1 deletion(-)

diff --git a/multisrc/overrides/madara/immortalupdates/src/ImmortalUpdates.kt b/multisrc/overrides/madara/immortalupdates/src/ImmortalUpdates.kt
index 5af840681..ec6222b11 100644
--- a/multisrc/overrides/madara/immortalupdates/src/ImmortalUpdates.kt
+++ b/multisrc/overrides/madara/immortalupdates/src/ImmortalUpdates.kt
@@ -1,7 +1,201 @@
 package eu.kanade.tachiyomi.extension.en.immortalupdates
 
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Paint
+import android.graphics.Rect
+import app.cash.quickjs.QuickJs
 import eu.kanade.tachiyomi.multisrc.madara.Madara
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.Page
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.ResponseBody.Companion.toResponseBody
+import org.jsoup.nodes.Document
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
 
 class ImmortalUpdates : Madara("Immortal Updates", "https://immortalupdates.com", "en") {
+
     override val useNewChapterEndpoint: Boolean = true
+
+    override val client = super.client.newBuilder().addInterceptor { chain ->
+        val response = chain.proceed(chain.request())
+
+        val args = response.request.url.queryParameter("iu_descramble")
+            ?.split(",")
+            ?: return@addInterceptor response
+
+        val image = unscrambleImage(response.body!!.byteStream(), args)
+        val body = image.toResponseBody("image/jpeg".toMediaTypeOrNull())
+        return@addInterceptor response.newBuilder()
+            .body(body)
+            .build()
+    }.build()
+
+    override fun pageListParse(document: Document): List<Page> {
+        val pageList = super.pageListParse(document).toMutableList()
+
+        val unscramblingCallsPage = pageList.firstOrNull { it.imageUrl!!.contains("00-call") }
+            ?: return pageList
+
+        val unscramblingCalls = client.newCall(GET(unscramblingCallsPage.imageUrl!!, headers))
+            .execute()
+            .use { it.body!!.string() }
+
+        unscramblingCalls.replace("\r", "").split("\n").forEach {
+            val args = unfuckJs(it)
+                .substringAfter("(")
+                .substringBefore(")")
+
+            val filenameFragment = args.split(",")[0].removeSurrounding("'")
+            val page = pageList.firstOrNull { it.imageUrl!!.contains(filenameFragment, ignoreCase = true) }
+                ?: return@forEach
+            val newPageUrl = page.imageUrl!!.toHttpUrl().newBuilder()
+                .addQueryParameter("iu_descramble", args)
+                .build().toString()
+            pageList[page.index] = Page(page.index, document.location(), newPageUrl)
+        }
+        pageList.remove(unscramblingCallsPage)
+        return pageList
+    }
+
+    // Converted from _0x3bc005: Find the CanvasRenderingContext2D.drawImage call basically
+    //
+    // `args` is the arguments of the original get_img call:
+    //     get_img(file_to_match, indexer, iterations, sectionWidth, sectionHeight, isBackgroundBlack, shouldFillColor, ???, key, keyAddition, ???)
+    //
+    // The boolean after shouldFillColor seems to always be `false` so I have optimized it out for now.
+    // If it fucks up someone will make an issue anyways /shrug
+    //
+    // I assumed the last argument was to check if versions match or something (since it was 1.0.1)
+    // but it was used in some canvas thingy that I didn't bother to check
+    private fun unscrambleImage(image: InputStream, args: List<String>): ByteArray {
+        val indexer = args[1].toInt()
+        val iterations = args[2].toInt()
+        val sectionWidth = args[3].toInt()
+        val sectionHeight = args[4].toInt()
+        val isBackgroundBlack = args[5] == "true"
+        val shouldFillColor = args[6] == "true"
+        val key = args[8].toInt()
+        val keyAddition = args[9].toInt()
+
+        val bitmap = BitmapFactory.decodeStream(image)
+
+        val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(result)
+
+        val heightSectionCount = bitmap.height / sectionHeight
+        val widthSectionCount = bitmap.width / sectionWidth
+        val sectionCount = heightSectionCount * widthSectionCount
+        val descramblingArray = createDescramblingArray(indexer, sectionCount, key, keyAddition, iterations)
+
+        if (shouldFillColor) {
+            val backgroundColor = if (isBackgroundBlack) Color.BLACK else Color.WHITE
+            canvas.drawColor(backgroundColor)
+        }
+
+        var i = 0
+        for (vertical in 0 until heightSectionCount) {
+            for (horizontal in 0 until widthSectionCount) {
+                val swap = descramblingArray[i]
+
+                val baseHeight = swap.floorDiv(widthSectionCount)
+                val baseWidth = swap - baseHeight * widthSectionCount
+
+                val dx = baseWidth * sectionWidth
+                val dy = baseHeight * sectionHeight
+
+                val sx = horizontal * sectionWidth
+                val sy = vertical * sectionHeight
+
+                val srcRect = Rect(sx, sy, sx + sectionWidth, sy + sectionHeight)
+                val dstRect = Rect(dx, dy, dx + sectionWidth, dy + sectionHeight)
+
+                canvas.drawBitmap(bitmap, srcRect, dstRect, null)
+                i += 1
+            }
+        }
+
+        val invertingPaint = Paint().apply {
+            colorFilter = ColorMatrixColorFilter(
+                ColorMatrix(
+                    floatArrayOf(
+                        -1.0f, 0.0f, 0.0f, 0.0f, 255.0f,
+                        0.0f, -1.0f, 0.0f, 0.0f, 255.0f,
+                        0.0f, 0.0f, -1.0f, 0.0f, 255.0f,
+                        0.0f, 0.0f, 0.0f, 1.0f, 0.0f
+                    )
+                )
+            )
+        }
+        canvas.drawBitmap(result, 0f, 0f, invertingPaint)
+
+        val output = ByteArrayOutputStream()
+        result.compress(Bitmap.CompressFormat.JPEG, 90, output)
+
+        return output.toByteArray()
+    }
+
+    // Converted from _0x144afb
+    // This should be called a little bit before the drawImage calls
+    private fun createDescramblingArray(indexer: Int, size: Int, key: Int, keyAddition: Int, iterations: Int = 2): List<Int> {
+        var indexerMut = indexer
+        val returnArray = mutableListOf<Int>()
+
+        for (i in 0 until size) {
+            returnArray.add(i)
+        }
+
+        for (i in 0 until size) {
+            for (o in 0 until iterations) {
+                indexerMut = (indexerMut * key + keyAddition) % size
+
+                val tmp = returnArray[indexerMut]
+                returnArray[indexerMut] = returnArray[i]
+                returnArray[i] = tmp
+            }
+        }
+
+        return returnArray
+    }
+
+    private fun unfuckJs(jsf: String): String {
+        // String: ([]+[])
+        // fontcolor: (![]+[])[+[]]+({}+[])[+!![]]+([][[]]+[])[+!![]]+(!![]+[])[+[]]+({}+[])[!![]+!![]+!![]+!![]+!![]]+({}+[])[+!![]]+(![]+[])[!![]+!![]]+({}+[])[+!![]]+(!![]+[])[+!![]]
+        // "undefined": []+[][[]]
+        // Quick hack so QuickJS doesn't complain about function being called with no args
+        val input = jsf.replace(
+            "([]+[])[(![]+[])[+[]]+({}+[])[+!![]]+([][[]]+[])[+!![]]+(!![]+[])[+[]]+({}+[])[!![]+!![]+!![]+!![]+!![]]+({}+[])[+!![]]+(![]+[])[!![]+!![]]+({}+[])[+!![]]+(!![]+[])[+!![]]]()",
+            "([]+[])[(![]+[])[+[]]+({}+[])[+!![]]+([][[]]+[])[+!![]]+(!![]+[])[+[]]+({}+[])[!![]+!![]+!![]+!![]+!![]]+({}+[])[+!![]]+(![]+[])[!![]+!![]]+({}+[])[+!![]]+(!![]+[])[+!![]]]([]+[][[]])",
+        )
+        return QuickJs.create().use {
+            it.execute(jsfBoilerplate)
+            it.evaluate(input.removePrefix("[]").removeSuffix("()") + "[0]").toString()
+        }
+    }
+
+    private val jsfBoilerplate: ByteArray by lazy {
+        QuickJs.create().use {
+            it.compile(
+                """
+                class Location {
+                    constructor(href) {
+                        this.href = href
+                    }
+
+                    toString() {
+                        return this.href
+                    }
+                }
+                this.location = new Location("https://");
+                """.trimIndent(),
+                "?"
+            )
+        }
+    }
 }
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
index 8883d14b9..243a4bc9a 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
@@ -148,7 +148,7 @@ class MadaraGenerator : ThemeSourceGenerator {
         SingleLang("Ichirin No Hana Yuri", "https://ichirinnohanayuriscan.com", "pt-BR", isNsfw = true, overrideVersionCode = 4),
         SingleLang("Ikifeng", "https://ikifeng.com", "es", isNsfw = true),
         SingleLang("Illusion Scan", "https://illusionscan.com", "pt-BR", isNsfw = true),
-        SingleLang("Immortal Updates", "https://immortalupdates.com", "en", overrideVersionCode = 2),
+        SingleLang("Immortal Updates", "https://immortalupdates.com", "en", overrideVersionCode = 3),
         SingleLang("Império Scans", "https://imperioscans.com.br", "pt-BR", className = "ImperioScans", overrideVersionCode = 1),
         SingleLang("Inazu Manga", "https://inazumanga.com", "id", isNsfw = true),
         SingleLang("InfraFandub", "https://infrafandub.xyz", "es"),