feat: remote media resolver

This commit is contained in:
rhunk 2023-06-01 22:48:06 +02:00
parent cb55923fd8
commit d3d8e22957
11 changed files with 67 additions and 63 deletions

View File

@ -1,5 +1,3 @@
import groovy.json.JsonSlurper
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
@ -102,4 +100,5 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.arthenica:ffmpeg-kit-full-gpl:5.1.LTS' implementation 'com.arthenica:ffmpeg-kit-full-gpl:5.1.LTS'
implementation 'org.osmdroid:osmdroid-android:6.1.16' implementation 'org.osmdroid:osmdroid-android:6.1.16'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
} }

View File

@ -13,7 +13,7 @@ object Constants {
val MESSAGE_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH = intArrayOf(3, 3, 5, 1, 1) 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_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_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 = 19
const val ARROYO_ENCRYPTION_PROTO_INDEX_V2 = 4 const val ARROYO_ENCRYPTION_PROTO_INDEX_V2 = 4

View File

@ -29,14 +29,13 @@ import me.rhunk.snapenhance.util.EncryptionUtils
import me.rhunk.snapenhance.util.MediaDownloaderHelper import me.rhunk.snapenhance.util.MediaDownloaderHelper
import me.rhunk.snapenhance.util.MediaType import me.rhunk.snapenhance.util.MediaType
import me.rhunk.snapenhance.util.PreviewUtils 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.getObjectField
import me.rhunk.snapenhance.util.protobuf.ProtoReader import me.rhunk.snapenhance.util.protobuf.ProtoReader
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
import java.lang.StringBuilder
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.nio.file.Paths import java.nio.file.Paths
@ -308,7 +307,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
for (i in 0 until baseUrlNodeList.length) { for (i in 0 until baseUrlNodeList.length) {
val baseUrlNode = baseUrlNodeList.item(i) val baseUrlNode = baseUrlNodeList.item(i)
val baseUrl = baseUrlNode.textContent val baseUrl = baseUrlNode.textContent
baseUrlNode.textContent = "${CdnDownloader.CF_ST_CDN_D}$baseUrl" baseUrlNode.textContent = "${RemoteMediaResolver.CF_ST_CDN_D}$baseUrl"
} }
val xmlData = ByteArrayOutputStream() val xmlData = ByteArrayOutputStream()
@ -405,14 +404,13 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
return return
} }
val messageReader = ProtoReader(message.message_content!!) 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 //download the message content
try { try {
context.shortToast("Querying $urlKey") val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(urlProto, canMergeOverlay(), isPreviewMode) {
val downloadedMedia = MediaDownloaderHelper.downloadMediaFromKey(urlKey, canMergeOverlay(), isPreviewMode) {
EncryptionUtils.decryptInputStreamFromArroyo(it, contentType, messageReader) 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) val fileType = FileType.fromByteArray(downloadedMedia)
if (isPreviewMode) { if (isPreviewMode) {

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.features.impl.extras package me.rhunk.snapenhance.features.impl.extras
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.FeatureLoadParams

View File

@ -189,9 +189,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
.flatten() .flatten()
mediaReferences.forEach { media -> 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 mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString)
val urlKey = ProtoReader(mediaContent).getString(2, 2) ?: return@forEach
runCatching { runCatching {
//download the media //download the media
val mediaInfo = ProtoReader(contentData).let { 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@let it.readPath(*Constants.MESSAGE_SNAP_ENCRYPTION_PROTO_PATH)
}?: return@runCatching }?: 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)) if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX))
EncryptionUtils.decryptInputStream(it, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX) EncryptionUtils.decryptInputStream(it, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX)
else it 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 bitmapPreview = PreviewUtils.createPreview(downloadedMedia, mediaType == MediaReferenceType.VIDEO)!!
val notificationBuilder = XposedHelpers.newInstance( val notificationBuilder = XposedHelpers.newInstance(
@ -220,7 +219,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
return@onEach return@onEach
}.onFailure { }.onFailure {
Logger.xposedLog("Failed to send preview notification", it) Logger.xposedLog("Failed to send preview notification", it)
Logger.xposedLog("urlKey: $urlKey")
} }
} }
} }

View File

@ -3,7 +3,6 @@ package me.rhunk.snapenhance.features.impl.ui.menus
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.widget.Switch import android.widget.Switch

View File

@ -5,12 +5,12 @@ import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.impl.ConfigEnumKeys 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.Messaging
import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader 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.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.AutoSave
import me.rhunk.snapenhance.features.impl.extras.DisableVideoLengthRestriction import me.rhunk.snapenhance.features.impl.extras.DisableVideoLengthRestriction
import me.rhunk.snapenhance.features.impl.extras.GalleryMediaSendOverride import me.rhunk.snapenhance.features.impl.extras.GalleryMediaSendOverride

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.mapping.impl package me.rhunk.snapenhance.mapping.impl
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.Logger.debug import me.rhunk.snapenhance.Logger.debug
import me.rhunk.snapenhance.mapping.Mapper import me.rhunk.snapenhance.mapping.Mapper
import java.lang.reflect.Method import java.lang.reflect.Method

View File

@ -3,7 +3,7 @@ package me.rhunk.snapenhance.util
import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFmpegKit
import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.data.FileType 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.ByteArrayInputStream
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -16,9 +16,9 @@ enum class MediaType {
ORIGINAL, OVERLAY ORIGINAL, OVERLAY
} }
object MediaDownloaderHelper { object MediaDownloaderHelper {
fun downloadMediaFromKey(key: String, mergeOverlay: Boolean, isPreviewMode: Boolean, decryptionCallback: (InputStream) -> InputStream): Map<MediaType, ByteArray> { fun downloadMediaFromReference(mediaReference: ByteArray, 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 inputStream: InputStream = RemoteMediaResolver.downloadBoltMedia(mediaReference) ?: throw FileNotFoundException("Unable to get media key. Check the logs for more info")
val content = decryptionCallback(inputStream).readBytes().also { inputStream.close() } val content = decryptionCallback(inputStream).readBytes()
val fileType = FileType.fromByteArray(content) val fileType = FileType.fromByteArray(content)
val isZipFile = fileType == FileType.ZIP val isZipFile = fileType == FileType.ZIP

View File

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

View File

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