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:
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())
+ }
+ }
+}