FMReader pt.3: Revenge of the Obfuscation (#7129)

* Attemped to fix fetchPageListEncrypted

* Remove ManhuaScan, HeroScan, ManhwaHot
This commit is contained in:
h-hyuuga 2021-05-21 08:39:39 -04:00 committed by GitHub
parent d2bf29c960
commit 35a77d7f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 0 additions and 180 deletions

View File

@ -1,4 +0,0 @@
dependencies {
implementation project(':lib-ratelimit')
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.extension.en.heroscan
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.OkHttpClient
class HeroScan : FMReader("HeroScan", "https://heroscan.com", "en") {
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(RateLimitInterceptor(1))
.addInterceptor { chain ->
val originalRequest = chain.request()
chain.proceed(originalRequest).let { response ->
if (response.code == 403 && originalRequest.url.host.contains("b-cdn")) {
response.close()
chain.proceed(originalRequest.newBuilder().removeHeader("Referer").addHeader("Referer", "https://isekaiscan.com").build())
} else {
response
}
}
}
.build()
override fun fetchPageList(chapter: SChapter) = fetchPageListEncrypted(chapter)
}

View File

@ -1,4 +0,0 @@
dependencies {
implementation project(':lib-ratelimit')
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.en.manhuascan
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.annotations.Nsfw
import okhttp3.OkHttpClient
@Nsfw
class ManhuaScan : FMReader("ManhuaScan", "https://manhuascan.com", "en") {
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(RateLimitInterceptor(1))
.build()
override fun fetchPageList(chapter: SChapter) = fetchPageListEncrypted(chapter)
}

View File

@ -1,4 +0,0 @@
dependencies {
implementation project(':lib-ratelimit')
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.en.manhwahot
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.OkHttpClient
@Nsfw
class ManhwaHot : FMReader("ManhwaHot", "https://manhwahot.com", "en") {
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(RateLimitInterceptor(1))
.build()
override fun fetchPageList(chapter: SChapter) = fetchPageListEncrypted(chapter)
}

View File

@ -1,10 +1,7 @@
package eu.kanade.tachiyomi.multisrc.fmreader package eu.kanade.tachiyomi.multisrc.fmreader
import android.util.Base64 import android.util.Base64
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
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
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -15,26 +12,14 @@ import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import okio.ByteString
import okio.ByteString.Companion.decodeBase64
import okio.ByteString.Companion.decodeHex
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Elements import org.jsoup.select.Elements
import rx.Observable
import java.nio.charset.Charset import java.nio.charset.Charset
import java.security.MessageDigest
import java.util.Calendar import java.util.Calendar
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/** /**
* For sites based on the Flat-Manga CMS * For sites based on the Flat-Manga CMS
*/ */
@ -328,32 +313,6 @@ abstract class FMReader(
} }
} }
/**
* fetches the pageList by decrypting and parsing the json response of a "chapter server"
* Meant to be used by relevant subclasses to replace their implementation of fetchPageList as needed
* e.g ManhuaScan, HeroScan
*/
protected fun fetchPageListEncrypted(chapter: SChapter): Observable<List<Page>> {
fun stringAssignment(varname: String, script: String) = Regex("""(?:let|var)\s+$varname\s*=\s*"([^"]*)"""").find(script)?.groups?.get(1)?.value
fun pageList(s: String) = Regex("https.+?(?=https|\"$)").findAll(s).map { it.groups[0]!!.value.replace("\\/", "/") }
fun pageListRequest(id: String, server: Int = 1) = POST("$baseUrl/app/manga/controllers/cont.chapterServer$server.php", headers, "id=$id".toRequestBody("application/x-www-form-urlencoded; charset=UTF-8".toMediaTypeOrNull()))
return client.newCall(GET("$baseUrl${chapter.url}", headers)).asObservableSuccess().concatMap { htmlResponse ->
val soup = htmlResponse.asJsoup()
soup.selectFirst("head > script[type='text/javascript']")?.data()?.let { params ->
stringAssignment("chapter_id", params)?.let { chapterId ->
client.newCall(pageListRequest(chapterId)).asObservableSuccess()
.map { jsonResponse ->
try {
pageList(crypto.aes_decrypt(jsonResponse.body!!.string(), "4xje8fvkub2d3mb5cy9rv661zyjakbcn".toByteArray()))
.mapIndexed { i, imgUrl -> Page(i, "", imgUrl) }.toList()
} catch (_: BadPaddingException) {
throw RuntimeException("Decryption Failed")
}
}
}
} ?: Observable.just(emptyList())
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
@ -513,74 +472,4 @@ abstract class FMReader(
Genre("Zombies") Genre("Zombies")
) )
companion object {
interface Crypto {
fun md5(s: String): String;
fun aes_decrypt(context: String, key: ByteArray): String;
}
/* cryptography utilities leveraged by fetchPageListEncrypted */
val crypto = object : Crypto {
private fun parseCryptoCT(s: String): Map<String, ByteString> {
// https://cryptojs.gitbook.io/docs/
val jsonObj = JsonParser.parseString(s).asJsonObject
val cipherParams = mutableMapOf("ciphertext" to jsonObj["ct"].asString.decodeBase64()!!)
jsonObj["iv"]?.let { cipherParams.put("iv", it.asString.decodeHex()) }
jsonObj["s"]?.let { cipherParams.put("salt", it.asString.decodeHex()) }
return cipherParams
}
private fun decryptAES(encrypted: ByteString, password: ByteArray, iv: ByteString, salt: ByteString): String {
// https://stackoverflow.com/questions/29151211/29152379#29152379
val (key, _iv) = EvpKDF(password, 256, 128, salt.toByteArray(), 1, "MD5")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(_iv))
return String(cipher.doFinal(encrypted.toByteArray()), Charset.forName("UTF-8"))
}
private fun EvpKDF(password: ByteArray, _keySize: Int, _ivSize: Int, salt: ByteArray, iterations: Int, hashAlgorithm: String): Pair<ByteArray, ByteArray> {
// Key Derivation Function
val keySize = _keySize / 32
val ivSize = _ivSize / 32
val targetKeySize = keySize + ivSize
val derivedBytes = ByteArray(targetKeySize * 4)
var numberOfDerivedWords = 0
var block = ByteArray(0)
val hasher = MessageDigest.getInstance(hashAlgorithm)
val key = ByteArray(keySize * 4)
val iv = ByteArray(ivSize * 4)
while (numberOfDerivedWords < targetKeySize) {
if (block != null) {
hasher.update(block)
}
hasher.update(password)
block = hasher.digest(salt)
hasher.reset()
// Iterations
for (i in 1 until iterations) {
block = hasher.digest(block)
hasher.reset()
}
System.arraycopy(block, 0, derivedBytes, numberOfDerivedWords * 4,
block.size.coerceAtMost((targetKeySize - numberOfDerivedWords) * 4))
numberOfDerivedWords += block.size / 4
}
System.arraycopy(derivedBytes, 0, key, 0, key.size)
System.arraycopy(derivedBytes, key.size, iv, 0, iv.size)
return key to iv // key + iv
}
override fun md5(s: String): String = MessageDigest.getInstance("MD5").digest(s.toByteArray()).joinToString("") { String.format("%02x", it) }
override fun aes_decrypt(context: String, key: ByteArray): String {
val cipherParams = parseCryptoCT(JsonParser.parseString(context).asJsonObject["content"].asString)
return decryptAES(cipherParams["ciphertext"]!!, key, cipherParams["iv"]!!, cipherParams["salt"]!!)
}
}
}
} }

View File

@ -17,17 +17,14 @@ class FMReaderGenerator : ThemeSourceGenerator {
override val sources = listOf( override val sources = listOf(
SingleLang("Epik Manga", "https://www.epikmanga.com", "tr"), SingleLang("Epik Manga", "https://www.epikmanga.com", "tr"),
SingleLang("HeroScan", "https://heroscan.com", "en", overrideVersionCode = 2),
SingleLang("KissLove", "https://kissaway.net", "ja"), SingleLang("KissLove", "https://kissaway.net", "ja"),
SingleLang("LHTranslation", "https://lhtranslation.net", "en", overrideVersionCode = 1), SingleLang("LHTranslation", "https://lhtranslation.net", "en", overrideVersionCode = 1),
SingleLang("Manga-TR", "https://manga-tr.com", "tr", className = "MangaTR"), SingleLang("Manga-TR", "https://manga-tr.com", "tr", className = "MangaTR"),
SingleLang("ManhuaScan", "https://manhuascan.com", "en", isNsfw = true, overrideVersionCode = 3),
SingleLang("Manhwa18", "https://manhwa18.com", "en", isNsfw = true), SingleLang("Manhwa18", "https://manhwa18.com", "en", isNsfw = true),
MultiLang("Manhwa18.net", "https://manhwa18.net", listOf("en", "ko"), className = "Manhwa18NetFactory", isNsfw = true), MultiLang("Manhwa18.net", "https://manhwa18.net", listOf("en", "ko"), className = "Manhwa18NetFactory", isNsfw = true),
SingleLang("RawLH", "https://lovehug.net", "ja"), SingleLang("RawLH", "https://lovehug.net", "ja"),
SingleLang("Say Truyen", "https://saytruyen.com", "vi"), SingleLang("Say Truyen", "https://saytruyen.com", "vi"),
SingleLang("KSGroupScans", "https://ksgroupscans.com", "en"), SingleLang("KSGroupScans", "https://ksgroupscans.com", "en"),
SingleLang("ManhwaHot", "https://manhwahot.com", "en", isNsfw = true, overrideVersionCode=1),
// Sites that went down // Sites that went down
//SingleLang("18LHPlus", "https://18lhplus.com", "en", className = "EighteenLHPlus"), //SingleLang("18LHPlus", "https://18lhplus.com", "en", className = "EighteenLHPlus"),
//SingleLang("HanaScan (RawQQ)", "https://hanascan.com", "ja", className = "HanaScanRawQQ"), //SingleLang("HanaScan (RawQQ)", "https://hanascan.com", "ja", className = "HanaScanRawQQ"),