add source and create decode url image

pull/214/head
devi 3 years ago
parent db96a1ff2e
commit f0fa8d59a1

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.parsers.site.madara
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import org.json.JSONObject
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext 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.exception.ParseException
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.* import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.cryptoaes.CryptoAES
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -291,6 +293,7 @@ internal abstract class MadaraParser(
val stateDiv = (body.selectFirst("div.post-content_item:contains(Status)") 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(Statut)")
?: body.selectFirst("div.post-content_item:contains(État)")
?: body.selectFirst("div.post-content_item:contains(حالة العمل)") ?: body.selectFirst("div.post-content_item:contains(حالة العمل)")
?: body.selectFirst("div.post-content_item:contains(Estado)") ?: body.selectFirst("div.post-content_item:contains(Estado)")
?: body.selectFirst("div.post-content_item:contains(สถานะ)") ?: body.selectFirst("div.post-content_item:contains(สถานะ)")
@ -392,18 +395,67 @@ internal abstract class MadaraParser(
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(domain) val fullUrl = chapter.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml() 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) val chapterProtector = doc.selectFirst("#chapter-protector-data")
return root.select("div.page-break").map { div ->
val img = div.selectFirst("img") ?: div.parseFailed("Page image not found") if (chapterProtector == null) {
val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found") val root = doc.body().selectFirst("div.main-col-inner")?.selectFirst("div.reading-content")
MangaPage( ?: throw ParseException("Root not found", fullUrl)
id = generateUid(url), return root.select("div.page-break").map { div ->
url = url, val img = div.selectFirst("img") ?: div.parseFailed("Page image not found")
preview = null, val url = img.src()?.toRelativeUrl(domain) ?: div.parseFailed("Image src not found")
source = source, 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 { 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) it.substring(0, pos) to it.substring(pos + 1)
}.toMutableMap() }.toMutableMap()
} }

@ -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)

@ -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/"
}

@ -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"
}

@ -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"
}

@ -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<MangaChapter> {
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,
)
}
}
}

@ -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"
}

@ -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<ByteArray?>? {
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<ByteArray>(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())
}
}
}
Loading…
Cancel
Save