diff --git a/multisrc/overrides/fmreader/heroscan/src/HeroScan.kt b/multisrc/overrides/fmreader/heroscan/src/HeroScan.kt index a03a0e800..801467252 100644 --- a/multisrc/overrides/fmreader/heroscan/src/HeroScan.kt +++ b/multisrc/overrides/fmreader/heroscan/src/HeroScan.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.extension.en.heroscan 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") { @@ -17,4 +18,5 @@ class HeroScan : FMReader("HeroScan", "https://heroscan.com", "en") { } } .build() + override fun fetchPageList(chapter: SChapter) = fetchPageListEncrypted(chapter) } diff --git a/multisrc/overrides/fmreader/manhuascan/src/ManhuaScan.kt b/multisrc/overrides/fmreader/manhuascan/src/ManhuaScan.kt new file mode 100644 index 000000000..79b8ac617 --- /dev/null +++ b/multisrc/overrides/fmreader/manhuascan/src/ManhuaScan.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.extension.en.manhuascan + +import eu.kanade.tachiyomi.multisrc.fmreader.FMReader +import eu.kanade.tachiyomi.source.model.SChapter + +import eu.kanade.tachiyomi.annotations.Nsfw + + +@Nsfw +class ManhuaScan : FMReader("ManhuaScan", "https://manhuascan.com", "en") { + override fun fetchPageList(chapter: SChapter) = fetchPageListEncrypted(chapter) +} diff --git a/multisrc/overrides/fmreader/manhwasmut/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/fmreader/manhwahot/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/fmreader/manhwasmut/res/mipmap-hdpi/ic_launcher.png rename to multisrc/overrides/fmreader/manhwahot/res/mipmap-hdpi/ic_launcher.png diff --git a/multisrc/overrides/fmreader/manhwasmut/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/fmreader/manhwahot/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/fmreader/manhwasmut/res/mipmap-mdpi/ic_launcher.png rename to multisrc/overrides/fmreader/manhwahot/res/mipmap-mdpi/ic_launcher.png diff --git a/multisrc/overrides/fmreader/manhwasmut/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/fmreader/manhwahot/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/fmreader/manhwasmut/res/mipmap-xhdpi/ic_launcher.png rename to multisrc/overrides/fmreader/manhwahot/res/mipmap-xhdpi/ic_launcher.png diff --git a/multisrc/overrides/fmreader/manhwasmut/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/fmreader/manhwahot/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/fmreader/manhwasmut/res/mipmap-xxhdpi/ic_launcher.png rename to multisrc/overrides/fmreader/manhwahot/res/mipmap-xxhdpi/ic_launcher.png diff --git a/multisrc/overrides/fmreader/manhwasmut/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/fmreader/manhwahot/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/fmreader/manhwasmut/res/mipmap-xxxhdpi/ic_launcher.png rename to multisrc/overrides/fmreader/manhwahot/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/multisrc/overrides/fmreader/manhwasmut/res/web_hi_res_512.png b/multisrc/overrides/fmreader/manhwahot/res/web_hi_res_512.png similarity index 100% rename from multisrc/overrides/fmreader/manhwasmut/res/web_hi_res_512.png rename to multisrc/overrides/fmreader/manhwahot/res/web_hi_res_512.png diff --git a/multisrc/overrides/fmreader/manhwahot/src/ManhwaHot.kt b/multisrc/overrides/fmreader/manhwahot/src/ManhwaHot.kt new file mode 100644 index 000000000..b9516e579 --- /dev/null +++ b/multisrc/overrides/fmreader/manhwahot/src/ManhwaHot.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.extension.en.manhwahot + +import eu.kanade.tachiyomi.annotations.Nsfw +import eu.kanade.tachiyomi.multisrc.fmreader.FMReader +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.Page +import okhttp3.Request +import eu.kanade.tachiyomi.source.model.SChapter + +@Nsfw +class ManhwaHot : FMReader("ManhwaHot", "https://manhwahot.com", "en") { + override fun fetchPageList(chapter: SChapter) = fetchPageListEncrypted(chapter) +} diff --git a/multisrc/overrides/fmreader/manhwasmut/src/ManhwaSmut.kt b/multisrc/overrides/fmreader/manhwasmut/src/ManhwaSmut.kt deleted file mode 100644 index 31aae1dad..000000000 --- a/multisrc/overrides/fmreader/manhwasmut/src/ManhwaSmut.kt +++ /dev/null @@ -1,13 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.manhwasmut - -import eu.kanade.tachiyomi.annotations.Nsfw -import eu.kanade.tachiyomi.multisrc.fmreader.FMReader -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.source.model.Page -import okhttp3.Request - -@Nsfw -class ManhwaSmut : FMReader("ManhwaSmut", "https://manhwasmut.com", "en") { - private val noReferer = headersBuilder().removeAll("Referer").build() - override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, if (page.imageUrl!!.contains("toonily")) noReferer else headers) -} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReader.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReader.kt index 94de1ea25..e60f291a0 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReader.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReader.kt @@ -1,7 +1,10 @@ package eu.kanade.tachiyomi.multisrc.fmreader import android.util.Base64 +import com.google.gson.JsonParser 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.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -12,14 +15,24 @@ import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.util.asJsoup import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody 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.Element import org.jsoup.select.Elements +import rx.Observable import java.nio.charset.Charset +import java.security.MessageDigest import java.util.Calendar +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec /** * For sites based on the Flat-Manga CMS @@ -209,7 +222,7 @@ abstract class FMReader( open val chapterUrlSelector = "a" - open val chapterTimeSelector = "time, .chapter-time" + open val chapterTimeSelector = "time, .chapter-time, .publishedDate" open val chapterNameAttrSelector = "title" @@ -314,6 +327,36 @@ 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> { + fun stringAssignment(varname: String, script: String) = Regex("""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 -> + val chapterId = stringAssignment("chapter_id", params) + val csrfToken = stringAssignment("csrf_token", params) + if (chapterId == null || csrfToken == null) + null + else + client.newCall(pageListRequest(chapterId)).asObservableSuccess() + .map { jsonResponse -> + pageList( + crypto.aes_decrypt( + jsonResponse.body!!.string(), + crypto.md5("$csrfToken$csrfToken").toByteArray() + ) + ).mapIndexed { i, imgUrl -> Page(i, "", imgUrl) }.toList() + } + } ?: Observable.just(emptyList()) + } + } + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") private class TextField(name: String, val key: String) : Filter.Text(name) @@ -471,4 +514,75 @@ abstract class FMReader( Genre("Western"), 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 { + // 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 { + // 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"]!!) + } + } + } } diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReaderGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReaderGenerator.kt index 7d8854a3a..237065b06 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReaderGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReaderGenerator.kt @@ -10,24 +10,24 @@ class FMReaderGenerator : ThemeSourceGenerator { override val themeClass = "FMReader" - override val baseVersionCode: Int = 3 + override val baseVersionCode: Int = 4 /** For future sources: when testing and popularMangaRequest() returns a Jsoup error instead of results * most likely the fix is to override popularMangaNextPageSelector() */ override val sources = listOf( SingleLang("Epik Manga", "https://www.epikmanga.com", "tr"), - SingleLang("HeroScan", "https://heroscan.com", "en"), + SingleLang("HeroScan", "https://heroscan.com", "en", overrideVersionCode = 1), SingleLang("KissLove", "https://kissaway.net", "ja"), SingleLang("LHTranslation", "https://lhtranslation.net", "en", overrideVersionCode = 1), SingleLang("Manga-TR", "https://manga-tr.com", "tr", className = "MangaTR"), - SingleLang("ManhuaScan", "https://manhuascan.com", "en", isNsfw = true, overrideVersionCode = 1), + SingleLang("ManhuaScan", "https://manhuascan.com", "en", isNsfw = true, overrideVersionCode = 2), SingleLang("Manhwa18", "https://manhwa18.com", "en", isNsfw = true), MultiLang("Manhwa18.net", "https://manhwa18.net", listOf("en", "ko"), className = "Manhwa18NetFactory", isNsfw = true), - SingleLang("ManhwaSmut", "https://manhwasmut.com", "en", isNsfw = true, overrideVersionCode = 2), SingleLang("RawLH", "https://lovehug.net", "ja"), SingleLang("Say Truyen", "https://saytruyen.com", "vi"), SingleLang("KSGroupScans", "https://ksgroupscans.com", "en"), + SingleLang("ManhwaHot", "https://manhwahot.com", "en", isNsfw = true), // Sites that went down //SingleLang("18LHPlus", "https://18lhplus.com", "en", className = "EighteenLHPlus"), //SingleLang("HanaScan (RawQQ)", "https://hanascan.com", "ja", className = "HanaScanRawQQ"),