RCO: decrypt page links in extension (#8634)
* Reimplemented pageListParse to decrypt the page in-app * remove unused assets --------- Co-authored-by: JakeeLuwi <jeloubirad@gmail.com>
This commit is contained in:
parent
f8a44eb538
commit
38f511b815
@ -1,56 +0,0 @@
|
||||
(() => {
|
||||
const isValidUrl = (str) => {
|
||||
try {
|
||||
new URL(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const arrays = [];
|
||||
const functions = [];
|
||||
const results = [];
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
const builtInKeys = new Set(Object.getOwnPropertyNames(iframe.contentWindow));
|
||||
document.body.removeChild(iframe);
|
||||
|
||||
for (const [key, value] of Object.entries(window)) {
|
||||
if (builtInKeys.has(key)) continue;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
arrays.push(value);
|
||||
} else if (typeof value === 'function' && value.length >= 1) {
|
||||
functions.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
arrays.forEach(arrayValue => {
|
||||
functions.forEach(funcValue => {
|
||||
const argCount = funcValue.length;
|
||||
|
||||
for (let i = 0; i < argCount; i++) {
|
||||
try {
|
||||
const mapped = arrayValue.map(elem => {
|
||||
const args = new Array(argCount).fill(undefined);
|
||||
args[i] = elem;
|
||||
return funcValue(...args);
|
||||
});
|
||||
|
||||
if (
|
||||
Array.isArray(mapped) &&
|
||||
mapped.length !== 0 &&
|
||||
mapped.every(item => typeof item === 'string' && isValidUrl(item))
|
||||
) {
|
||||
results.push(mapped);
|
||||
break;
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
})();
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'ReadComicOnline'
|
||||
extClass = '.Readcomiconline'
|
||||
extVersionCode = 34
|
||||
extVersionCode = 35
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,23 +1,10 @@
|
||||
package eu.kanade.tachiyomi.extension.en.readcomiconline
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import eu.kanade.tachiyomi.lib.randomua.UserAgentType
|
||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@ -26,11 +13,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.tryParse
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
@ -39,12 +22,8 @@ import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class Readcomiconline : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
@ -58,6 +37,8 @@ class Readcomiconline : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
private val scriptPageRegex = """(?s)pth\s*=\s*['"](.*?)['"]\s*;?""".toRegex()
|
||||
private val urlDecryptionRegex = """l\s*\.replace\(\s*/(.*?)/([gimuy]*)\s*,\s*(['"`])(.*?)\3\s*\)""".toRegex()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.setRandomUserAgent(
|
||||
@ -234,123 +215,45 @@ class Readcomiconline : ConfigurableSource, ParsedHttpSource() {
|
||||
return GET(baseUrl + chapter.url + qualitySuffix, headers)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val latch = CountDownLatch(1)
|
||||
var webView: WebView? = null
|
||||
var urls: List<List<String>> = emptyList()
|
||||
// Declare some important values first
|
||||
val encryptedLinks = mutableListOf<String>()
|
||||
val decryptionRegexKeys = mutableListOf<Pair<String, String>>()
|
||||
|
||||
handler.post {
|
||||
val innerWv = WebView(Injekt.get<Application>())
|
||||
// Get script elements
|
||||
val scripts = document.select("script[type=text/javascript]")
|
||||
|
||||
webView = innerWv
|
||||
innerWv.settings.javaScriptEnabled = true
|
||||
innerWv.settings.blockNetworkImage = true
|
||||
innerWv.settings.domStorageEnabled = true
|
||||
innerWv.settings.userAgentString = headers["User-Agent"]
|
||||
innerWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||
// We'll get a bunch of results on the selector but we only need 2: The script that contains the encrypted links and the script
|
||||
// that contains the partial decryption key.
|
||||
for (script in scripts) {
|
||||
val scriptContent = script.data()
|
||||
if (scriptContent.isNotEmpty()) {
|
||||
val encryptedValues = scriptPageRegex.findAll(scriptContent)
|
||||
val decryptionKeys = urlDecryptionRegex.findAll(scriptContent)
|
||||
|
||||
innerWv.webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): WebResourceResponse? {
|
||||
val emptyResponse = WebResourceResponse("text/plain", "utf-8", 200, "OK", mapOf(), "".byteInputStream())
|
||||
// We found the encrypted links
|
||||
if (encryptedValues.count() > 0) {
|
||||
encryptedValues.forEach {
|
||||
val url = it.groupValues[1]
|
||||
|
||||
val url = request?.url?.toString()
|
||||
|
||||
url ?: return emptyResponse
|
||||
|
||||
if (!url.contains("rguard")) {
|
||||
return emptyResponse
|
||||
}
|
||||
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
view?.evaluateJavascript(
|
||||
Readcomiconline::class.java
|
||||
.getResourceAsStream("/assets/script.js")!!
|
||||
.bufferedReader()
|
||||
.use { it.readText() },
|
||||
) {
|
||||
try {
|
||||
urls = it.parseAs()
|
||||
latch.countDown()
|
||||
} catch (e: Exception) {
|
||||
Log.e("RCO", e.stackTraceToString())
|
||||
if (url.isNotBlank()) {
|
||||
encryptedLinks.add(url)
|
||||
}
|
||||
}
|
||||
|
||||
super.onLoadResource(view, url)
|
||||
}
|
||||
}
|
||||
|
||||
innerWv.webChromeClient = object : WebChromeClient() {
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
if (consoleMessage == null) { return false }
|
||||
val logContent = "wv: ${consoleMessage.message()} (${consoleMessage.sourceId()}, line ${consoleMessage.lineNumber()})"
|
||||
when (consoleMessage.messageLevel()) {
|
||||
ConsoleMessage.MessageLevel.DEBUG -> Log.d("RCO", logContent)
|
||||
ConsoleMessage.MessageLevel.ERROR -> Log.e("RCO", logContent)
|
||||
ConsoleMessage.MessageLevel.LOG -> Log.i("RCO", logContent)
|
||||
ConsoleMessage.MessageLevel.TIP -> Log.i("RCO", logContent)
|
||||
ConsoleMessage.MessageLevel.WARNING -> Log.w("RCO", logContent)
|
||||
else -> Log.d("RCO", logContent)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
innerWv.loadDataWithBaseURL(
|
||||
document.location(),
|
||||
document.outerHtml(),
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
latch.await(30, TimeUnit.SECONDS)
|
||||
handler.post { webView?.destroy() }
|
||||
|
||||
if (latch.count == 1L) {
|
||||
throw Exception("Timeout getting image links")
|
||||
}
|
||||
|
||||
val images = runBlocking {
|
||||
val valid = urls.map { urlList ->
|
||||
async {
|
||||
val request = Request.Builder()
|
||||
.url(urlList.random())
|
||||
.method("HEAD", null)
|
||||
.headers(headers)
|
||||
.build()
|
||||
|
||||
try {
|
||||
val response = client.newCall(request).await()
|
||||
val contentType = response.headers["content-type"] ?: ""
|
||||
val size = response.headers["content-length"]?.toLongOrNull() ?: 0L
|
||||
|
||||
response.isSuccessful && contentType.startsWith("image") && size > 100
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
// We found the keys
|
||||
if (decryptionKeys.count() > 0) {
|
||||
decryptionKeys.forEach {
|
||||
// Corresponds to Pair<RegexPattern, ReplacementValue>
|
||||
decryptionRegexKeys.add(Pair(it.groupValues[1], it.groupValues[4]))
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
val index = valid.indexOf(true)
|
||||
if (index == -1) {
|
||||
throw Exception("No valid image links found")
|
||||
}
|
||||
urls[index]
|
||||
}
|
||||
|
||||
return images.mapIndexed { idx, img ->
|
||||
Page(idx, imageUrl = img)
|
||||
return encryptedLinks.mapIndexed { idx, rawUrl ->
|
||||
Page(idx, imageUrl = decryptLink(rawUrl, decryptionRegexKeys, ""))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,71 @@
|
||||
package eu.kanade.tachiyomi.extension.en.readcomiconline
|
||||
|
||||
import android.util.Base64
|
||||
import java.net.URLDecoder
|
||||
|
||||
private fun step1(param: String): String {
|
||||
return param.substring(15, 15 + 18) + param.substring(15 + 18 + 17)
|
||||
}
|
||||
|
||||
private fun step2(param: String): String {
|
||||
return param.substring(0, param.length - (9 + 2)) +
|
||||
param[param.length - 2] +
|
||||
param[param.length - 1]
|
||||
}
|
||||
|
||||
fun decryptLink(
|
||||
firstStringFormat: String,
|
||||
partialDecryptKeys: List<Pair<String, String>>,
|
||||
formatter: String = "",
|
||||
): String {
|
||||
var processedString = firstStringFormat
|
||||
|
||||
partialDecryptKeys.forEach {
|
||||
processedString = processedString.replace(it.first.toRegex(), it.second)
|
||||
}
|
||||
|
||||
processedString = processedString
|
||||
.replace("pw_.g28x", "b")
|
||||
.replace("d2pr.x_27", "h")
|
||||
|
||||
if (!processedString.startsWith("https")) {
|
||||
val firstStringFormatLocalVar = processedString
|
||||
val firstStringSubS = firstStringFormatLocalVar.substring(
|
||||
firstStringFormatLocalVar.indexOf("?"),
|
||||
)
|
||||
|
||||
processedString = if (firstStringFormatLocalVar.contains("=s0?")) {
|
||||
firstStringFormatLocalVar.substring(0, firstStringFormatLocalVar.indexOf("=s0?"))
|
||||
} else {
|
||||
firstStringFormatLocalVar.substring(0, firstStringFormatLocalVar.indexOf("=s1600?"))
|
||||
}
|
||||
|
||||
processedString = step1(processedString)
|
||||
processedString = step2(processedString)
|
||||
|
||||
// Base64 decode and URL decode
|
||||
val decodedBytes = Base64.decode(processedString, Base64.DEFAULT)
|
||||
processedString = URLDecoder.decode(String(decodedBytes), "UTF-8")
|
||||
|
||||
processedString = processedString.substring(0, 13) +
|
||||
processedString.substring(17)
|
||||
|
||||
processedString = if (firstStringFormat.contains("=s0")) {
|
||||
processedString.substring(0, processedString.length - 2) + "=s0"
|
||||
} else {
|
||||
processedString.substring(0, processedString.length - 2) + "=s1600"
|
||||
}
|
||||
|
||||
processedString += firstStringSubS
|
||||
processedString = "https://2.bp.blogspot.com/$processedString"
|
||||
}
|
||||
|
||||
if (formatter.isNotEmpty()) {
|
||||
processedString = processedString.replace(
|
||||
"https://2.bp.blogspot.com",
|
||||
formatter,
|
||||
)
|
||||
}
|
||||
|
||||
return processedString
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user