package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64DecodeArray import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import java.nio.charset.StandardCharsets import java.security.MessageDigest import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec // No License found in https://github.com/enimax-anime/key // special credits to @enimax for providing key class Megacloud : Rabbitstream() { override val name = "Megacloud" override val mainUrl = "https://megacloud.tv" override val embed = "embed-2/ajax/e-1" override val key = "https://raw.githubusercontent.com/enimax-anime/key/e6/key.txt" } class Dokicloud : Rabbitstream() { override val name = "Dokicloud" override val mainUrl = "https://dokicloud.one" } open class Rabbitstream : ExtractorApi() { override val name = "Rabbitstream" override val mainUrl = "https://rabbitstream.net" override val requiresReferer = false open val embed = "ajax/embed-4" open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt" override suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { val id = url.substringAfterLast("/").substringBefore("?") val response = app.get( "$mainUrl/$embed/getSources?id=$id", referer = mainUrl, headers = mapOf("X-Requested-With" to "XMLHttpRequest") ) val encryptedMap = response.parsedSafe() val sources = encryptedMap?.sources val decryptedSources = if (sources == null || encryptedMap.encrypted == false) { response.parsedSafe() } else { val (key, encData) = extractRealKey(sources, getRawKey()) val decrypted = decryptMapped>(encData, key) SourcesResponses( sources = decrypted, tracks = encryptedMap.tracks ) } decryptedSources?.sources?.map { source -> M3u8Helper.generateM3u8( name, source?.file ?: return@map, "$mainUrl/", ).forEach(callback) } decryptedSources?.tracks?.map { track -> subtitleCallback.invoke( SubtitleFile( track?.label ?: "", track?.file ?: return@map ) ) } } private suspend fun getRawKey(): String = app.get(key).text private fun extractRealKey(originalString: String?, stops: String): Pair { val table = parseJson>>(stops) val decryptedKey = StringBuilder() var offset = 0 var encryptedString = originalString table.forEach { (start, end) -> decryptedKey.append(encryptedString?.substring(start - offset, end - offset)) encryptedString = encryptedString?.substring( 0, start - offset ) + encryptedString?.substring(end - offset) offset += end - start } return decryptedKey.toString() to encryptedString.toString() } private inline fun decryptMapped(input: String, key: String): T? { val decrypt = decrypt(input, key) return AppUtils.tryParseJson(decrypt) } private fun decrypt(input: String, key: String): String { return decryptSourceUrl( generateKey( base64DecodeArray(input).copyOfRange(8, 16), key.toByteArray() ), input ) } private fun generateKey(salt: ByteArray, secret: ByteArray): ByteArray { var key = md5(secret + salt) var currentKey = key while (currentKey.size < 48) { key = md5(key + secret + salt) currentKey += key } return currentKey } private fun md5(input: ByteArray): ByteArray { return MessageDigest.getInstance("MD5").digest(input) } private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String { val cipherData = base64DecodeArray(sourceUrl) val encrypted = cipherData.copyOfRange(16, cipherData.size) val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding") aesCBC.init( Cipher.DECRYPT_MODE, SecretKeySpec(decryptionKey.copyOfRange(0, 32), "AES"), IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) ) val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found") return String(decryptedData, StandardCharsets.UTF_8) } data class Tracks( @JsonProperty("file") val file: String? = null, @JsonProperty("label") val label: String? = null, @JsonProperty("kind") val kind: String? = null, ) data class Sources( @JsonProperty("file") val file: String? = null, @JsonProperty("type") val type: String? = null, @JsonProperty("label") val label: String? = null, ) data class SourcesResponses( @JsonProperty("sources") val sources: List? = emptyList(), @JsonProperty("tracks") val tracks: List? = emptyList(), ) data class SourcesEncrypted( @JsonProperty("sources") val sources: String? = null, @JsonProperty("encrypted") val encrypted: Boolean? = null, @JsonProperty("tracks") val tracks: List? = emptyList(), ) }