From f0fa8d59a15ba3cd1ac690a3c3e56fcd7edbc61d Mon Sep 17 00:00:00 2001 From: devi Date: Tue, 18 Jul 2023 19:12:25 +0200 Subject: [PATCH] add source and create decode url image --- .../parsers/site/madara/MadaraParser.kt | 73 ++++++++-- .../parsers/site/madara/en/MangaCultivator.kt | 12 ++ .../parsers/site/madara/en/Manhuasy.kt | 15 ++ .../site/madara/pt/ApenasmaisumYaoi.kt | 14 ++ .../parsers/site/madara/pt/Leitorizakaya.kt | 14 ++ .../parsers/site/madara/pt/LimaScans.kt | 56 ++++++++ .../parsers/site/madara/pt/SinensisScans.kt | 13 ++ .../kotatsu/parsers/util/CryptoAES.kt | 132 ++++++++++++++++++ 8 files changed, 319 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaCultivator.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhuasy.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/ApenasmaisumYaoi.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/Leitorizakaya.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/LimaScans.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/SinensisScans.kt create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt index 592148be..2e714433 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.parsers.site.madara import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import org.json.JSONObject import org.jsoup.nodes.Document import org.jsoup.nodes.Element import org.koitharu.kotatsu.parsers.MangaLoaderContext @@ -10,6 +11,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.util.cryptoaes.CryptoAES import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -291,6 +293,7 @@ internal abstract class MadaraParser( val stateDiv = (body.selectFirst("div.post-content_item:contains(Status)") ?: body.selectFirst("div.post-content_item:contains(Statut)") + ?: body.selectFirst("div.post-content_item:contains(État)") ?: body.selectFirst("div.post-content_item:contains(حالة العمل)") ?: body.selectFirst("div.post-content_item:contains(Estado)") ?: body.selectFirst("div.post-content_item:contains(สถานะ)") @@ -392,18 +395,67 @@ internal abstract class MadaraParser( override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val root = doc.body().selectFirst("div.main-col-inner")?.selectFirst("div.reading-content") - ?: throw ParseException("Root not found", fullUrl) - return root.select("div.page-break").map { div -> - val img = div.selectFirst("img") ?: div.parseFailed("Page image not found") - val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found") - MangaPage( - id = generateUid(url), - url = url, - preview = null, - source = source, + + val chapterProtector = doc.selectFirst("#chapter-protector-data") + + if (chapterProtector == null) { + val root = doc.body().selectFirst("div.main-col-inner")?.selectFirst("div.reading-content") + ?: throw ParseException("Root not found", fullUrl) + return root.select("div.page-break").map { div -> + val img = div.selectFirst("img") ?: div.parseFailed("Page image not found") + val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found") + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } else { + + + val chapterProtectorHtml = chapterProtector.html() + val password = chapterProtectorHtml.substringAfter("wpmangaprotectornonce='").substringBefore("';") + val chapterData = JSONObject( + chapterProtectorHtml.substringAfter("chapter_data='").substringBefore("';").replace("\\/", "/"), ) + val unsaltedCiphertext = context.decodeBase64(chapterData.getString("ct")) + val salt = chapterData.getString("s").toString().decodeHex() + val ciphertext = SALTED + salt + unsaltedCiphertext + + val rawImgArray = CryptoAES.decrypt(Base64.getEncoder().encodeToString(ciphertext), password) + val imgArrayString = rawImgArray + .replace("[", "") + .replace("]", "") + .replace("\\", "") + .replace("\"", "") + + + return imgArrayString.split(",").map { url -> + MangaPage( + id = generateUid(url.toString()), + url = url.toString(), + preview = null, + source = source, + ) + } + } + + + } + + fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + companion object { + const val URL_SEARCH_PREFIX = "slug:" + val SALTED = "Salted__".toByteArray(Charsets.UTF_8) } protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { @@ -605,4 +657,5 @@ internal abstract class MadaraParser( it.substring(0, pos) to it.substring(pos + 1) }.toMutableMap() + } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaCultivator.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaCultivator.kt new file mode 100644 index 00000000..c9715fce --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaCultivator.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.parsers.site.madara.en + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.madara.MadaraParser + + +@MangaSourceParser("MANGACULTIVATOR", "MangaCultivator", "en") +internal class MangaCultivator(context: MangaLoaderContext) : + MadaraParser(context, MangaSource.MANGACULTIVATOR, "mangacultivator.com", 10) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhuasy.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhuasy.kt new file mode 100644 index 00000000..2c8af0e8 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/Manhuasy.kt @@ -0,0 +1,15 @@ +package org.koitharu.kotatsu.parsers.site.madara.en + + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.madara.MadaraParser + + +@MangaSourceParser("MANHUASY", "Manhuasy", "en") +internal class Manhuasy(context: MangaLoaderContext) : + MadaraParser(context, MangaSource.MANHUASY, "www.manhuasy.com") { + + override val tagPrefix = "manhua-genre/" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/ApenasmaisumYaoi.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/ApenasmaisumYaoi.kt new file mode 100644 index 00000000..fa7b34ca --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/ApenasmaisumYaoi.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.parsers.site.madara.pt + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.madara.MadaraParser + +@MangaSourceParser("APENASMAISUMYAOI", "ApenasmaisumYaoi", "pt") +internal class ApenasmaisumYaoi(context: MangaLoaderContext) : + MadaraParser(context, MangaSource.APENASMAISUMYAOI, "apenasmaisumyaoi.com") { + + override val isNsfwSource = true + override val datePattern: String = "dd 'de' MMMMM 'de' yyyy" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/Leitorizakaya.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/Leitorizakaya.kt new file mode 100644 index 00000000..324d25ac --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/Leitorizakaya.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.parsers.site.madara.pt + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.madara.MadaraParser + +@MangaSourceParser("LEITORIZAKAYA", "Leitorizakaya", "pt") +internal class Leitorizakaya(context: MangaLoaderContext) : + MadaraParser(context, MangaSource.LEITORIZAKAYA, "leitorizakaya.net") { + + override val isNsfwSource = true + override val datePattern: String = "dd/MM/yyyy" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/LimaScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/LimaScans.kt new file mode 100644 index 00000000..f9a83668 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/LimaScans.kt @@ -0,0 +1,56 @@ +package org.koitharu.kotatsu.parsers.site.madara.pt + +import org.jsoup.nodes.Document +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.madara.MadaraParser +import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrlOrNull +import org.koitharu.kotatsu.parsers.util.domain +import org.koitharu.kotatsu.parsers.util.generateUid +import org.koitharu.kotatsu.parsers.util.mapChapters +import org.koitharu.kotatsu.parsers.util.parseFailed +import org.koitharu.kotatsu.parsers.util.parseHtml +import java.text.SimpleDateFormat + +@MangaSourceParser("LIMASCANS", "Lima Scans", "pt") +internal class LimaScans(context: MangaLoaderContext) : + MadaraParser(context, MangaSource.LIMASCANS, "limascans.xyz/v2", 10) { + + override val isNsfwSource = true + override val postreq = true + override val datePattern: String = "dd 'de' MMMMM 'de' yyyy" + + override suspend fun loadChapters(mangaUrl: String, document: Document): List { + + + val mangaId = document.select("div#manga-chapters-holder").attr("data-id") + val url = "https://$domain/wp-admin/admin-ajax.php" + val postdata = "action=manga_get_chapters&manga=$mangaId" + val doc = webClient.httpPost(url, postdata).parseHtml() + + val dateFormat = SimpleDateFormat(datePattern, sourceLocale) + + return doc.select(selectchapter).mapChapters(reversed = true) { i, li -> + val a = li.selectFirst("a") + val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing") + val link = href + stylepage + val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectdate)?.text() + val name = a.selectFirst("p")?.text() ?: a.ownText() + MangaChapter( + id = generateUid(href), + url = link.replace("/v2", ""), + name = name, + number = i + 1, + branch = null, + uploadDate = parseChapterDate( + dateFormat, + dateText, + ), + scanlator = null, + source = source, + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/SinensisScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/SinensisScans.kt new file mode 100644 index 00000000..c60811cf --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/pt/SinensisScans.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.parsers.site.madara.pt + +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.site.madara.MadaraParser + +@MangaSourceParser("SINENSISSCANS", "Sinensis Scans", "pt") +internal class SinensisScans(context: MangaLoaderContext) : + MadaraParser(context, MangaSource.SINENSISSCANS, "sinensisscans.com") { + + override val datePattern: String = "dd/MM/yyyy" +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt new file mode 100644 index 00000000..9f707593 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/util/CryptoAES.kt @@ -0,0 +1,132 @@ +package org.koitharu.kotatsu.parsers.util.cryptoaes + +import java.security.MessageDigest +import java.util.Arrays +import java.util.Base64 +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +/** + * Conforming with CryptoJS AES method + */ +@Suppress("unused", "FunctionName") +object CryptoAES { + + private const val KEY_SIZE = 256 + private const val IV_SIZE = 128 + private const val HASH_CIPHER = "AES/CBC/PKCS7PADDING" + private const val AES = "AES" + private const val KDF_DIGEST = "MD5" + + /** + * Decrypt using CryptoJS defaults compatible method. + * Uses KDF equivalent to OpenSSL's EVP_BytesToKey function + * + * http://stackoverflow.com/a/29152379/4405051 + * @param cipherText base64 encoded ciphertext + * @param password passphrase + */ + fun decrypt(cipherText: String, password: String): String { + try { + val ctBytes = Base64.getDecoder().decode(cipherText) + val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16) + val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size) + val md5: MessageDigest = MessageDigest.getInstance("MD5") + val keyAndIV = generateKeyAndIV(32, 16, 1, saltBytes, password.toByteArray(Charsets.UTF_8), md5) + return decryptAES(cipherTextBytes, + keyAndIV?.get(0) ?: ByteArray(32), + keyAndIV?.get(1) ?: ByteArray(16)) + } catch (e: Exception) { + return "" + } + } + + /** + * Decrypt using CryptoJS defaults compatible method. + * + * @param cipherText base64 encoded ciphertext + * @param keyBytes key as a bytearray + * @param ivBytes iv as a bytearray + */ + fun decrypt(cipherText: String, keyBytes: ByteArray, ivBytes: ByteArray): String { + return try { + val cipherTextBytes = Base64.getDecoder().decode(cipherText) + decryptAES(cipherTextBytes, keyBytes, ivBytes) + } catch (e: Exception) { + "" + } + } + + /** + * Decrypt using CryptoJS defaults compatible method. + * + * @param cipherTextBytes encrypted text as a bytearray + * @param keyBytes key as a bytearray + * @param ivBytes iv as a bytearray + */ + private fun decryptAES(cipherTextBytes: ByteArray, keyBytes: ByteArray, ivBytes: ByteArray): String { + return try { + val cipher = Cipher.getInstance(HASH_CIPHER) + val keyS = SecretKeySpec(keyBytes, AES) + cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(ivBytes)) + cipher.doFinal(cipherTextBytes).toString(Charsets.UTF_8) + } catch (e: Exception) { + "" + } + } + + /** + * Generates a key and an initialization vector (IV) with the given salt and password. + * + * https://stackoverflow.com/a/41434590 + * This method is equivalent to OpenSSL's EVP_BytesToKey function + * (see https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c). + * By default, OpenSSL uses a single iteration, MD5 as the algorithm and UTF-8 encoded password data. + * + * @param keyLength the length of the generated key (in bytes) + * @param ivLength the length of the generated IV (in bytes) + * @param iterations the number of digestion rounds + * @param salt the salt data (8 bytes of data or `null`) + * @param password the password data (optional) + * @param md the message digest algorithm to use + * @return an two-element array with the generated key and IV + */ + private fun generateKeyAndIV(keyLength: Int, ivLength: Int, iterations: Int, salt: ByteArray, password: ByteArray, md: MessageDigest): Array? { + val digestLength = md.digestLength + val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + return try { + md.reset() + + // Repeat process until sufficient data has been generated + while (generatedLength < keyLength + ivLength) { + + // Digest data (last digest if available, password data, salt if available) + if (generatedLength > 0) md.update(generatedData, generatedLength - digestLength, digestLength) + md.update(password) + if (salt != null) md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + // additional rounds + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + generatedLength += digestLength + } + + // Copy key and IV into separate byte arrays + val result = arrayOfNulls(2) + result[0] = generatedData.copyOfRange(0, keyLength) + if (ivLength > 0) result[1] = generatedData.copyOfRange(keyLength, keyLength + ivLength) + result + } catch (e: Exception) { + throw e + } finally { + // Clean out temporary data + Arrays.fill(generatedData, 0.toByte()) + } + } +}