JapScan: yeet WebView (#14881)

* JapScan: Yeet WebView

* chore: remove unused regex
This commit is contained in:
beerpsi 2023-01-11 06:33:26 +07:00 committed by GitHub
parent 47d02ef732
commit 339f6c9f68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 41 additions and 175 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Japscan' extName = 'Japscan'
pkgNameSuffix = 'fr.japscan' pkgNameSuffix = 'fr.japscan'
extClass = '.Japscan' extClass = '.Japscan'
extVersionCode = 34 extVersionCode = 35
} }
dependencies { dependencies {

View File

@ -1,22 +1,10 @@
package eu.kanade.tachiyomi.extension.fr.japscan package eu.kanade.tachiyomi.extension.fr.japscan
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri import android.net.Uri
import android.os.Handler import android.util.Base64
import android.os.Looper
import android.util.Log import android.util.Log
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
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.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@ -34,23 +22,17 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.CyclicBarrier
class Japscan : ConfigurableSource, ParsedHttpSource() { class Japscan : ConfigurableSource, ParsedHttpSource() {
@ -70,79 +52,7 @@ class Japscan : ConfigurableSource, ParsedHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
internal class JsObject(private val latch: CountDownLatch, var width: Int = 0, var height: Int = 0) { override val client: OkHttpClient = network.cloudflareClient
@JavascriptInterface
fun passSize(widthjs: Int, ratio: Float) {
Log.d("japscan", "wvsc js returned $widthjs, $ratio")
width = widthjs
height = (width.toFloat() / ratio).toInt()
latch.countDown()
}
}
@SuppressLint("SetJavaScriptEnabled")
override val client: OkHttpClient = network.cloudflareClient.newBuilder().addInterceptor { chain ->
val indicator = "&wvsc"
val cleanupjs = "var checkExist=setInterval(function(){if(document.getElementsByTagName('CNV-VV').length){clearInterval(checkExist);var e=document.body,a=e.children;for(e.appendChild(document.getElementsByTagName('CNV-VV')[0]);'CNV-VV'!=a[0].tagName;)e.removeChild(a[0]);for(var t of[].slice.call(a[0].all_canvas))t.style.maxWidth='100%';window.android.passSize(a[0].all_canvas[0].width,a[0].all_canvas[0].width/a[0].all_canvas[0].height)}},100);"
val request = chain.request()
val url = request.url.toString()
val newRequest = request.newBuilder()
.url(url.substringBefore(indicator))
.build()
val response = chain.proceed(newRequest)
if (!url.endsWith(indicator)) return@addInterceptor response
// Webview screenshotting code
val handler = Handler(Looper.getMainLooper())
val latch = CountDownLatch(1)
var webView: WebView? = null
var height = 0
var width = 0
val jsinterface = JsObject(latch)
Log.d("japscan", "init wvsc")
handler.post {
val webview = WebView(Injekt.get<Application>())
webView = webview
webview.settings.javaScriptEnabled = true
webview.settings.domStorageEnabled = true
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
webview.settings.useWideViewPort = false
webview.settings.loadWithOverviewMode = false
webview.settings.userAgentString = webview.settings.userAgentString.replace("Mobile", "eliboM").replace("Android", "diordnA")
webview.addJavascriptInterface(jsinterface, "android")
var retries = 1
webview.webChromeClient = object : WebChromeClient() {
@SuppressLint("NewApi")
override fun onProgressChanged(view: WebView, progress: Int) {
if (progress == 100 && retries--> 0) {
Log.d("japscan", "wvsc loading finished")
view.evaluateJavascript(cleanupjs) {}
}
}
}
webview.loadUrl(url.replace("&wvsc", ""))
}
latch.await()
width = jsinterface.width
height = jsinterface.height
// webView!!.isDrawingCacheEnabled = true
webView!!.measure(width, height)
webView!!.layout(0, 0, width, height)
Thread.sleep(350)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
webView!!.draw(canvas)
// val bitmap: Bitmap = webView!!.drawingCache
val output = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
val rb = output.toByteArray().toResponseBody("image/png".toMediaTypeOrNull())
handler.post { webView!!.destroy() }
response.newBuilder().body(rb).build()
}.build()
companion object { companion object {
val dateFormat by lazy { val dateFormat by lazy {
@ -344,100 +254,56 @@ class Japscan : ConfigurableSource, ParsedHttpSource() {
} }
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
// no webview screenshot needed anymore : /*
// document.getElementsByTag("option").mapIndexed { i, it -> Page(i, "", baseUrl + it.attr("value") + "&wvsc") } JapScan stores chapter metadata in a `#data` element, and in the `data-data` attribute.
val zjsurl = document.getElementsByTag("script").first { it.attr("src").contains("zjs", ignoreCase = true) }.attr("src") This data is scrambled base64, and to unscramble it this code searches in the ZJS for
two strings of length 62 (base64 minus `+` and `/`), creating a character map.
Since figuring out how to properly map characters would be more effort than I want to
put in, this just flips around the charsets if the first attempt didn't succeed.
*/
val zjsurl = document.getElementsByTag("script").first {
it.attr("src").contains("zjs", ignoreCase = true)
}.attr("src")
Log.d("japscan", "ZJS at $zjsurl") Log.d("japscan", "ZJS at $zjsurl")
val zjs = client.newCall(GET(baseUrl + zjsurl, headers)).execute().body!!.string() val zjs = client.newCall(GET(baseUrl + zjsurl, headers)).execute().body!!.string()
Log.d("japscan", "webtoon, netdumping initiated")
val pagesElement = document.getElementById("pages") val strings = zjs
var pagecount = pagesElement.getElementsByTag("option").size .substringAfter("= [")
.substringBefore("];")
.split(",")
.map { it.trim().removeSurrounding("'").reversed() }
Log.d("japscan", "fallback $pagecount") val stringLookupTables = strings.filter {
it.length == 62 &&
if (pagecount == 0) { it.toCharArray().sorted().joinToString("") == "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
Log.d("japscan", "pagecount not found, fallback 1")
val element = document.select(".card:first-child .card-body p").toString()
val regex = """Pages<\/span>: ([0-9]+)<\/p>""".toRegex()
val matchResult = regex.find(element)
val (pagecountFromRegex) = matchResult!!.destructured
pagecount = pagecountFromRegex.toInt()
Log.d("japscan", "fallback pagecount with regex, result: $pagecount")
} }
val pages = ArrayList<Page>() if (stringLookupTables.size > 2) {
val handler = Handler(Looper.getMainLooper()) throw Exception("Expected only two lookup tables in ZJS")
val checkNew = ArrayList<String>(pagecount)
var maxIter = pagecount
var isSinglePage = false
if ((zjs.lowercase(Locale.ROOT).split("new image").size - 1) == 1) {
isSinglePage = true
maxIter = 1
} }
var webView: WebView? = null Log.d("japscan", "lookup tables: $stringLookupTables")
val dummyimage = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
val dummystream = ByteArrayOutputStream()
dummyimage.compress(Bitmap.CompressFormat.JPEG, 100, dummystream)
val barrier = CyclicBarrier(2)
for (i in 0 until maxIter) { val scrambledData = document.getElementById("data").attr("data-data")
handler.post {
if (webView == null) {
val webview = WebView(Injekt.get<Application>())
webView = webview
webview.settings.javaScriptEnabled = true
webview.settings.domStorageEnabled = true
webview.settings.userAgentString = webview.settings.userAgentString.replace("Mobile", "eliboM").replace("Android", "diordnA")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String?) {
if (pagecount === 0) {
Log.d("japscan", "pagecount not found, fallback 2")
Log.d("japscan", "dynamic page count detected, loading it through JS")
super.onPageFinished(view, url)
view.evaluateJavascript(
"(function() { return document.getElementById('pages').length; })();",
object : ValueCallback<String> {
override fun onReceiveValue(numberOfPages: String?) {
pagecount = numberOfPages!!.toInt()
}
}
)
}
}
override fun shouldInterceptRequest( for (i in 0..1) {
view: WebView, Log.d("japscan", "descramble attempt $i")
request: WebResourceRequest val otherIndice = if (i == 0) 1 else 0
): WebResourceResponse? { val lookupTable = stringLookupTables[i].zip(stringLookupTables[otherIndice]).toMap()
if (request.url.toString().startsWith("https://cdn.statically.io/img/c.japscan.ws/") && !checkNew.contains(request.url.toString())) { try {
checkNew.add(request.url.toString()) val unscrambledData = scrambledData.map { lookupTable[it] ?: it }.joinToString("")
pages.add(Page(pages.size, "", request.url.toString())) val decoded = Base64.decode(unscrambledData, Base64.DEFAULT).toString(Charsets.UTF_8)
Log.d("japscan", "intercepted ${request.url}")
if (pages.size == pagecount || !isSinglePage) { val data = json.parseToJsonElement(decoded).jsonObject
barrier.await()
} return data["imagesLink"]!!.jsonArray.mapIndexed { idx, it ->
return WebResourceResponse("image/jpeg", "UTF-8", ByteArrayInputStream(dummystream.toByteArray())) Page(idx, imageUrl = it.jsonPrimitive.content)
}
return super.shouldInterceptRequest(view, request)
}
}
} }
if (isSinglePage) { } catch (_: Exception) {}
webView?.loadUrl(baseUrl + document.select("li[^data-]").first().dataset()["chapter-url"])
} else {
webView?.loadUrl(baseUrl + pagesElement.getElementsByTag("option")[i].attr("value"))
}
}
barrier.await()
} }
handler.post { webView!!.destroy() }
return pages throw Exception("Both descrambling attempts failed")
} }
override fun imageUrlParse(document: Document): String = "" override fun imageUrlParse(document: Document): String = ""