commit d3d8e22957af1617b25982d4f23dc7d9a06c98cf
parent cb55923fd86dd5b5fb36799d483b9c4f98a7855b
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu,  1 Jun 2023 22:48:06 +0200

feat: remote media resolver

Diffstat:
Mapp/build.gradle | 3+--
Mapp/src/main/kotlin/me/rhunk/snapenhance/Constants.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 12+++++-------
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/MediaQualityLevelOverride.kt | 1-
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt | 8+++-----
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt | 1-
Mapp/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt | 1-
Mapp/src/main/kotlin/me/rhunk/snapenhance/util/MediaDownloaderHelper.kt | 8++++----
Dapp/src/main/kotlin/me/rhunk/snapenhance/util/download/CdnDownloader.kt | 39---------------------------------------
Aapp/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 67 insertions(+), 63 deletions(-)

diff --git a/app/build.gradle b/app/build.gradle @@ -1,5 +1,3 @@ -import groovy.json.JsonSlurper - plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' @@ -102,4 +100,5 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.arthenica:ffmpeg-kit-full-gpl:5.1.LTS' implementation 'org.osmdroid:osmdroid-android:6.1.16' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/Constants.kt b/app/src/main/kotlin/me/rhunk/snapenhance/Constants.kt @@ -13,7 +13,7 @@ object Constants { val MESSAGE_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH = intArrayOf(3, 3, 5, 1, 1) val ARROYO_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH = intArrayOf(4, 4, 3, 3, 5, 1, 1) val ARROYO_STRING_CHAT_MESSAGE_PROTO = intArrayOf(4, 4, 2, 1) - val ARROYO_URL_KEY_PROTO_PATH = intArrayOf(4, 5, 1, 3, 2, 2) + val ARROYO_URL_KEY_PROTO_PATH = intArrayOf(4, 5, 1, 3) const val ARROYO_ENCRYPTION_PROTO_INDEX = 19 const val ARROYO_ENCRYPTION_PROTO_INDEX_V2 = 4 diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -29,14 +29,13 @@ import me.rhunk.snapenhance.util.EncryptionUtils import me.rhunk.snapenhance.util.MediaDownloaderHelper import me.rhunk.snapenhance.util.MediaType import me.rhunk.snapenhance.util.PreviewUtils -import me.rhunk.snapenhance.util.download.CdnDownloader +import me.rhunk.snapenhance.util.download.RemoteMediaResolver import me.rhunk.snapenhance.util.getObjectField import me.rhunk.snapenhance.util.protobuf.ProtoReader import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.io.InputStream -import java.lang.StringBuilder import java.net.HttpURLConnection import java.net.URL import java.nio.file.Paths @@ -308,7 +307,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam for (i in 0 until baseUrlNodeList.length) { val baseUrlNode = baseUrlNodeList.item(i) val baseUrl = baseUrlNode.textContent - baseUrlNode.textContent = "${CdnDownloader.CF_ST_CDN_D}$baseUrl" + baseUrlNode.textContent = "${RemoteMediaResolver.CF_ST_CDN_D}$baseUrl" } val xmlData = ByteArrayOutputStream() @@ -405,14 +404,13 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam return } val messageReader = ProtoReader(message.message_content!!) - val urlKey: String = messageReader.getString(*ARROYO_URL_KEY_PROTO_PATH)!! + val urlProto: ByteArray = messageReader.getByteArray(*ARROYO_URL_KEY_PROTO_PATH)!! //download the message content try { - context.shortToast("Querying $urlKey") - val downloadedMedia = MediaDownloaderHelper.downloadMediaFromKey(urlKey, canMergeOverlay(), isPreviewMode) { + val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(urlProto, canMergeOverlay(), isPreviewMode) { EncryptionUtils.decryptInputStreamFromArroyo(it, contentType, messageReader) - }[MediaType.ORIGINAL] ?: throw Exception("Failed to download media for key $urlKey") + }[MediaType.ORIGINAL] ?: throw Exception("Failed to download media") val fileType = FileType.fromByteArray(downloadedMedia) if (isPreviewMode) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/MediaQualityLevelOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/MediaQualityLevelOverride.kt @@ -1,6 +1,5 @@ package me.rhunk.snapenhance.features.impl.extras -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt @@ -189,9 +189,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN .flatten() mediaReferences.forEach { media -> - val mediaContent = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() + val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) - val urlKey = ProtoReader(mediaContent).getString(2, 2) ?: return@forEach runCatching { //download the media val mediaInfo = ProtoReader(contentData).let { @@ -201,11 +200,11 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN return@let it.readPath(*Constants.MESSAGE_SNAP_ENCRYPTION_PROTO_PATH) }?: return@runCatching - val downloadedMedia = MediaDownloaderHelper.downloadMediaFromKey(urlKey, mergeOverlay = false, isPreviewMode = false) { + val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference, mergeOverlay = false, isPreviewMode = false) { if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX)) EncryptionUtils.decryptInputStream(it, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX) else it - }[MediaType.ORIGINAL] ?: throw Throwable("Failed to download media from key $urlKey") + }[MediaType.ORIGINAL] ?: throw Throwable("Failed to download media") val bitmapPreview = PreviewUtils.createPreview(downloadedMedia, mediaType == MediaReferenceType.VIDEO)!! val notificationBuilder = XposedHelpers.newInstance( @@ -220,7 +219,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN return@onEach }.onFailure { Logger.xposedLog("Failed to send preview notification", it) - Logger.xposedLog("urlKey: $urlKey") } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt @@ -3,7 +3,6 @@ package me.rhunk.snapenhance.features.impl.ui.menus import android.annotation.SuppressLint import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.Typeface import android.view.Gravity import android.view.View import android.widget.Switch diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -5,12 +5,12 @@ import me.rhunk.snapenhance.ModContext import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.ConfigEnumKeys -import me.rhunk.snapenhance.features.impl.experiments.MeoPasscodeBypass import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader -import me.rhunk.snapenhance.features.impl.extras.AntiAutoSave import me.rhunk.snapenhance.features.impl.experiments.AppPasscode +import me.rhunk.snapenhance.features.impl.experiments.MeoPasscodeBypass +import me.rhunk.snapenhance.features.impl.extras.AntiAutoSave import me.rhunk.snapenhance.features.impl.extras.AutoSave import me.rhunk.snapenhance.features.impl.extras.DisableVideoLengthRestriction import me.rhunk.snapenhance.features.impl.extras.GalleryMediaSendOverride diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt @@ -1,6 +1,5 @@ package me.rhunk.snapenhance.mapping.impl -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.Logger.debug import me.rhunk.snapenhance.mapping.Mapper import java.lang.reflect.Method diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/MediaDownloaderHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/MediaDownloaderHelper.kt @@ -3,7 +3,7 @@ package me.rhunk.snapenhance.util import com.arthenica.ffmpegkit.FFmpegKit import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.util.download.CdnDownloader +import me.rhunk.snapenhance.util.download.RemoteMediaResolver import java.io.ByteArrayInputStream import java.io.File import java.io.FileInputStream @@ -16,9 +16,9 @@ enum class MediaType { ORIGINAL, OVERLAY } object MediaDownloaderHelper { - fun downloadMediaFromKey(key: String, mergeOverlay: Boolean, isPreviewMode: Boolean, decryptionCallback: (InputStream) -> InputStream): Map<MediaType, ByteArray> { - val inputStream: InputStream = CdnDownloader.downloadWithDefaultEndpoints(key) ?: throw FileNotFoundException("Unable to get $key from cdn list. Check the logs for more info") - val content = decryptionCallback(inputStream).readBytes().also { inputStream.close() } + fun downloadMediaFromReference(mediaReference: ByteArray, mergeOverlay: Boolean, isPreviewMode: Boolean, decryptionCallback: (InputStream) -> InputStream): Map<MediaType, ByteArray> { + val inputStream: InputStream = RemoteMediaResolver.downloadBoltMedia(mediaReference) ?: throw FileNotFoundException("Unable to get media key. Check the logs for more info") + val content = decryptionCallback(inputStream).readBytes() val fileType = FileType.fromByteArray(content) val isZipFile = fileType == FileType.ZIP diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/download/CdnDownloader.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/download/CdnDownloader.kt @@ -1,39 +0,0 @@ -package me.rhunk.snapenhance.util.download - -import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.util.protobuf.ProtoWriter -import java.io.InputStream -import java.net.URL -import java.util.Base64 -import javax.net.ssl.HttpsURLConnection - -object CdnDownloader { - private const val BOLT_HTTP_RESOLVER_URL = "https://aws.api.snapchat.com/bolt-http" - const val CF_ST_CDN_D = "https://cf-st.sc-cdn.net/d/" - - private fun queryRemoteContent(url: String): InputStream? { - try { - val connection = URL(url).openConnection() as HttpsURLConnection - connection.requestMethod = "GET" - connection.instanceFollowRedirects = true - connection.setRequestProperty("User-Agent", Constants.USER_AGENT) - return connection.inputStream - } catch (ignored: Throwable) { - } - return null - } - - fun downloadWithDefaultEndpoints(key: String): InputStream? { - val payload = ProtoWriter().apply { - write(2) { - writeString(2, key) - writeBuffer(3, byteArrayOf()) - writeBuffer(3, byteArrayOf()) - writeConstant(6, 6) - writeConstant(10, 4) - writeConstant(12, 1) - } - }.toByteArray() - return queryRemoteContent(BOLT_HTTP_RESOLVER_URL + "/resolve?co=" + Base64.getUrlEncoder().encodeToString(payload)) - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt @@ -0,0 +1,51 @@ +package me.rhunk.snapenhance.util.download + +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.Logger +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.util.Base64 + +object RemoteMediaResolver { + private const val BOLT_HTTP_RESOLVER_URL = "https://aws.api.snapchat.com/bolt-http" + const val CF_ST_CDN_D = "https://cf-st.sc-cdn.net/d/" + + private val urlCache = mutableMapOf<String, String>() + + private val okHttpClient = OkHttpClient.Builder() + .followRedirects(true) + .addInterceptor { chain -> + val request = chain.request() + val requestUrl = request.url.toString() + + if (urlCache.containsKey(requestUrl)) { + val cachedUrl = urlCache[requestUrl]!! + return@addInterceptor chain.proceed(request.newBuilder().url(cachedUrl).build()) + } + + chain.proceed(request).apply { + val responseUrl = this.request.url.toString() + if (responseUrl.startsWith("https://cf-st.sc-cdn.net")) { + urlCache[requestUrl] = responseUrl + } + } + } + .build() + + fun downloadBoltMedia(protoKey: ByteArray): InputStream? { + val request = Request.Builder() + .url(BOLT_HTTP_RESOLVER_URL + "/resolve?co=" + Base64.getUrlEncoder().encodeToString(protoKey)) + .addHeader("User-Agent", Constants.USER_AGENT) + .build() + + okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + Logger.log("Unexpected code $response") + return null + } + return ByteArrayInputStream(response.body.bytes()) + } + } +}