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 {
|
ext {
|
||||||
extName = 'ReadComicOnline'
|
extName = 'ReadComicOnline'
|
||||||
extClass = '.Readcomiconline'
|
extClass = '.Readcomiconline'
|
||||||
extVersionCode = 34
|
extVersionCode = 35
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -1,23 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.readcomiconline
|
package eu.kanade.tachiyomi.extension.en.readcomiconline
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
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.UserAgentType
|
||||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import keiyoushi.utils.parseAs
|
|
||||||
import keiyoushi.utils.tryParse
|
import keiyoushi.utils.tryParse
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -39,12 +22,8 @@ import okhttp3.Response
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
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.TimeUnit
|
|
||||||
|
|
||||||
class Readcomiconline : ConfigurableSource, ParsedHttpSource() {
|
class Readcomiconline : ConfigurableSource, ParsedHttpSource() {
|
||||||
|
|
||||||
@ -58,6 +37,8 @@ class Readcomiconline : ConfigurableSource, ParsedHttpSource() {
|
|||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("Referer", "$baseUrl/")
|
.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()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.setRandomUserAgent(
|
.setRandomUserAgent(
|
||||||
@ -234,123 +215,45 @@ class Readcomiconline : ConfigurableSource, ParsedHttpSource() {
|
|||||||
return GET(baseUrl + chapter.url + qualitySuffix, headers)
|
return GET(baseUrl + chapter.url + qualitySuffix, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
val handler = Handler(Looper.getMainLooper())
|
// Declare some important values first
|
||||||
val latch = CountDownLatch(1)
|
val encryptedLinks = mutableListOf<String>()
|
||||||
var webView: WebView? = null
|
val decryptionRegexKeys = mutableListOf<Pair<String, String>>()
|
||||||
var urls: List<List<String>> = emptyList()
|
|
||||||
|
|
||||||
handler.post {
|
// Get script elements
|
||||||
val innerWv = WebView(Injekt.get<Application>())
|
val scripts = document.select("script[type=text/javascript]")
|
||||||
|
|
||||||
webView = innerWv
|
// 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
|
||||||
innerWv.settings.javaScriptEnabled = true
|
// that contains the partial decryption key.
|
||||||
innerWv.settings.blockNetworkImage = true
|
for (script in scripts) {
|
||||||
innerWv.settings.domStorageEnabled = true
|
val scriptContent = script.data()
|
||||||
innerWv.settings.userAgentString = headers["User-Agent"]
|
if (scriptContent.isNotEmpty()) {
|
||||||
innerWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
val encryptedValues = scriptPageRegex.findAll(scriptContent)
|
||||||
|
val decryptionKeys = urlDecryptionRegex.findAll(scriptContent)
|
||||||
|
|
||||||
innerWv.webViewClient = object : WebViewClient() {
|
// We found the encrypted links
|
||||||
override fun shouldInterceptRequest(
|
if (encryptedValues.count() > 0) {
|
||||||
view: WebView?,
|
encryptedValues.forEach {
|
||||||
request: WebResourceRequest?,
|
val url = it.groupValues[1]
|
||||||
): WebResourceResponse? {
|
|
||||||
val emptyResponse = WebResourceResponse("text/plain", "utf-8", 200, "OK", mapOf(), "".byteInputStream())
|
|
||||||
|
|
||||||
val url = request?.url?.toString()
|
if (url.isNotBlank()) {
|
||||||
|
encryptedLinks.add(url)
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onLoadResource(view, url)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
innerWv.webChromeClient = object : WebChromeClient() {
|
// We found the keys
|
||||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
if (decryptionKeys.count() > 0) {
|
||||||
if (consoleMessage == null) { return false }
|
decryptionKeys.forEach {
|
||||||
val logContent = "wv: ${consoleMessage.message()} (${consoleMessage.sourceId()}, line ${consoleMessage.lineNumber()})"
|
// Corresponds to Pair<RegexPattern, ReplacementValue>
|
||||||
when (consoleMessage.messageLevel()) {
|
decryptionRegexKeys.add(Pair(it.groupValues[1], it.groupValues[4]))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
|
||||||
|
|
||||||
val index = valid.indexOf(true)
|
|
||||||
if (index == -1) {
|
|
||||||
throw Exception("No valid image links found")
|
|
||||||
}
|
}
|
||||||
urls[index]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return images.mapIndexed { idx, img ->
|
return encryptedLinks.mapIndexed { idx, rawUrl ->
|
||||||
Page(idx, imageUrl = img)
|
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