From 00fbd318e772863c2d6b8e806d07764627f62450 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Tue, 31 Dec 2024 21:17:51 +0900 Subject: [PATCH] feat(YouTube - Spoof streaming data): Add setting to change `PoToken / Visitor Data` https://github.com/inotia00/ReVanced_Extended/issues/2630#issuecomment-2566310025 --- .../shared/patches/client/AppClient.java | 12 +++++ .../spoof/SpoofStreamingDataPatch.java | 26 ++++++++++- .../patches/spoof/requests/PlayerRoutes.java | 11 +---- .../spoof/requests/StreamingDataRequest.java | 44 ++++++++++++++----- .../shared/settings/BaseSettings.java | 3 ++ .../video/requests/PlaylistRequest.java | 5 ++- .../BaseSpoofStreamingDataPatch.kt | 19 ++++++++ .../spoof/streamingdata/Fingerprints.kt | 15 +++++++ .../youtube/settings/host/values/strings.xml | 13 ++++++ .../youtube/settings/xml/revanced_prefs.xml | 6 +++ 10 files changed, 132 insertions(+), 22 deletions(-) diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java index d8dc963e0..c67094f29 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java @@ -181,6 +181,7 @@ public class AppClient { ANDROID_SDK_VERSION_ANDROID_VR, CLIENT_VERSION_ANDROID_VR, true, + false, "Android VR" ), ANDROID_UNPLUGGED(29, @@ -190,6 +191,7 @@ public class AppClient { ANDROID_SDK_VERSION_ANDROID_UNPLUGGED, CLIENT_VERSION_ANDROID_UNPLUGGED, true, + false, "Android TV" ), IOS_UNPLUGGED(33, @@ -199,6 +201,7 @@ public class AppClient { null, CLIENT_VERSION_IOS_UNPLUGGED, true, + false, forceAVC() ? "iOS TV Force AVC" : "iOS TV" @@ -210,6 +213,7 @@ public class AppClient { null, CLIENT_VERSION_IOS, false, + true, forceAVC() ? "iOS Force AVC" : "iOS" @@ -222,6 +226,7 @@ public class AppClient { null, CLIENT_VERSION_IOS_MUSIC, true, + false, "iOS Music" ); @@ -265,6 +270,11 @@ public class AppClient { */ public final boolean canLogin; + /** + * If a poToken should be used. + */ + public final boolean usePoToken; + /** * Friendly name displayed in stats for nerds. */ @@ -277,6 +287,7 @@ public class AppClient { @Nullable String androidSdkVersion, String clientVersion, boolean canLogin, + boolean usePoToken, String friendlyName ) { this.id = id; @@ -287,6 +298,7 @@ public class AppClient { this.androidSdkVersion = androidSdkVersion; this.userAgent = userAgent; this.canLogin = canLogin; + this.usePoToken = usePoToken; this.friendlyName = friendlyName; } diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java index f1ca98e32..227f01435 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java @@ -4,7 +4,9 @@ import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDa import android.net.Uri; import android.text.TextUtils; +import android.util.Base64; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.nio.ByteBuffer; @@ -19,7 +21,11 @@ import app.revanced.extension.shared.utils.Utils; @SuppressWarnings("unused") public class SpoofStreamingDataPatch { - public static final boolean SPOOF_STREAMING_DATA = SpoofStreamingData() && BaseSettings.SPOOF_STREAMING_DATA.get(); + private static final boolean SPOOF_STREAMING_DATA = SpoofStreamingData() && BaseSettings.SPOOF_STREAMING_DATA.get(); + private static final String PO_TOKEN = + BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get(); + private static final String VISITOR_DATA = + BaseSettings.SPOOF_STREAMING_DATA_VISITOR_DATA.get(); /** * Any unreachable ip address. Used to intentionally fail requests. @@ -27,6 +33,9 @@ public class SpoofStreamingDataPatch { private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); + @NonNull + private static volatile String droidGuardPoToken = ""; + /** * Key: video id * Value: original video length [streamingData.formats.approxDurationMs] @@ -128,7 +137,7 @@ public class SpoofStreamingDataPatch { return; } - StreamingDataRequest.fetchRequest(id, requestHeaders); + StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN, droidGuardPoToken); } } catch (Exception ex) { Logger.printException(() -> "buildRequest failure", ex); @@ -253,4 +262,17 @@ public class SpoofStreamingDataPatch { return videoFormat; } + + /** + * Injection point. + */ + public static void setDroidGuardPoToken(byte[] bytes) { + if (SPOOF_STREAMING_DATA && bytes.length > 20) { + final String poToken = Base64.encodeToString(bytes, Base64.URL_SAFE); + if (!droidGuardPoToken.equals(poToken)) { + Logger.printDebug(() -> "New droidGuardPoToken loaded:\n" + poToken); + droidGuardPoToken = poToken; + } + } + } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java index 4eb16d20c..af2eb167c 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java @@ -37,11 +37,7 @@ public final class PlayerRoutes { private PlayerRoutes() { } - public static String createInnertubeBody(ClientType clientType) { - return createInnertubeBody(clientType, false); - } - - public static String createInnertubeBody(ClientType clientType, boolean playlistId) { + public static JSONObject createInnertubeBody(ClientType clientType) { JSONObject innerTubeBody = new JSONObject(); try { @@ -66,14 +62,11 @@ public final class PlayerRoutes { innerTubeBody.put("contentCheckOk", true); innerTubeBody.put("racyCheckOk", true); innerTubeBody.put("videoId", "%s"); - if (playlistId) { - innerTubeBody.put("playlistId", "%s"); - } } catch (JSONException e) { Logger.printException(() -> "Failed to create innerTubeBody", e); } - return innerTubeBody.toString(); + return innerTubeBody; } /** diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java index fc9cf3bc8..b4da8c61c 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java @@ -44,10 +44,11 @@ public class StreamingDataRequest { private static final ClientType[] CLIENT_ORDER_TO_USE; private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String VISITOR_ID_HEADER = "X-Goog-Visitor-Id"; private static final String[] REQUEST_HEADER_KEYS = { AUTHORIZATION_HEADER, // Available only to logged-in users. "X-GOOG-API-FORMAT-VERSION", - "X-Goog-Visitor-Id" + VISITOR_ID_HEADER }; private static ClientType lastSpoofedClientType; @@ -105,15 +106,17 @@ public class StreamingDataRequest { private final String videoId; private final Future future; - private StreamingDataRequest(String videoId, Map playerHeaders) { + private StreamingDataRequest(String videoId, Map playerHeaders, String visitorId, + String botGuardPoToken, String droidGuardPoToken) { Objects.requireNonNull(playerHeaders); this.videoId = videoId; - this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); + this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders, visitorId, botGuardPoToken, droidGuardPoToken)); } - public static void fetchRequest(String videoId, Map fetchHeaders) { + public static void fetchRequest(String videoId, Map fetchHeaders, String visitorId, + String botGuardPoToken, String droidGuardPoToken) { // Always fetch, even if there is an existing request for the same video. - cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders, visitorId, botGuardPoToken, droidGuardPoToken)); } @Nullable @@ -126,8 +129,8 @@ public class StreamingDataRequest { } @Nullable - private static HttpURLConnection send(ClientType clientType, String videoId, - Map playerHeaders) { + private static HttpURLConnection send(ClientType clientType, String videoId, Map playerHeaders, + String visitorId, String botGuardPoToken, String droidGuardPoToken) { Objects.requireNonNull(clientType); Objects.requireNonNull(videoId); Objects.requireNonNull(playerHeaders); @@ -149,12 +152,32 @@ public class StreamingDataRequest { continue; } } + if (key.equals(VISITOR_ID_HEADER) && + clientType.usePoToken && + !botGuardPoToken.isEmpty() && + !visitorId.isEmpty()) { + String originalVisitorId = value; + Logger.printDebug(() -> "Original visitor id:\n" + originalVisitorId); + Logger.printDebug(() -> "Replaced visitor id:\n" + visitorId); + value = visitorId; + } connection.setRequestProperty(key, value); } } - String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + JSONObject innerTubeBodyJson = PlayerRoutes.createInnertubeBody(clientType); + if (clientType.usePoToken && !botGuardPoToken.isEmpty() && !visitorId.isEmpty()) { + JSONObject serviceIntegrityDimensions = new JSONObject(); + serviceIntegrityDimensions.put("poToken", botGuardPoToken); + innerTubeBodyJson.put("serviceIntegrityDimensions", serviceIntegrityDimensions); + if (!droidGuardPoToken.isEmpty()) { + Logger.printDebug(() -> "Original poToken (droidGuardPoToken):\n" + droidGuardPoToken); + } + Logger.printDebug(() -> "Replaced poToken (botGuardPoToken):\n" + botGuardPoToken); + } + + String innerTubeBody = String.format(innerTubeBodyJson.toString(), videoId); byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); connection.setFixedLengthStreamingMode(requestBody.length); connection.getOutputStream().write(requestBody); @@ -180,12 +203,13 @@ public class StreamingDataRequest { return null; } - private static ByteBuffer fetch(String videoId, Map playerHeaders) { + private static ByteBuffer fetch(String videoId, Map playerHeaders, String visitorId, + String botGuardPoToken, String droidGuardPoToken) { lastSpoofedClientType = null; // Retry with different client if empty response body is received. for (ClientType clientType : CLIENT_ORDER_TO_USE) { - HttpURLConnection connection = send(clientType, videoId, playerHeaders); + HttpURLConnection connection = send(clientType, videoId, playerHeaders, visitorId, botGuardPoToken, droidGuardPoToken); if (connection != null) { try { // gzip encoding doesn't response with content length (-1), diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index 0e0c9dd39..886852d2e 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -42,6 +42,9 @@ public class BaseSettings { // Client type must be last spoof setting due to cyclic references. public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true); + public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true); + public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true); + /** * @noinspection DeprecatedIsStillUsed */ diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java index b7c69cd0a..db12d6d23 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java @@ -84,9 +84,12 @@ public class PlaylistRequest { try { HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType); + JSONObject innerTubeBodyJson = PlayerRoutes.createInnertubeBody(clientType); + innerTubeBodyJson.put("playlistId", "%s"); + String innerTubeBody = String.format( Locale.ENGLISH, - PlayerRoutes.createInnertubeBody(clientType, true), + innerTubeBodyJson.toString(), videoId, "RD" + videoId ); diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt index 0ddda9abf..8c023ac2c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt @@ -21,6 +21,7 @@ import app.revanced.util.fingerprint.definingClassOrThrow import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall import app.revanced.util.fingerprint.matchOrThrow import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags @@ -362,6 +363,24 @@ fun baseSpoofStreamingDataPatch( // endregion + // region Set DroidGuard poToken. + + poTokenToStringFingerprint.mutableClassOrThrow().let { + val poTokenClass = it.fields.find { field -> + field.accessFlags == AccessFlags.PRIVATE.value && field.type.startsWith("L") + }!!.type + + findMethodOrThrow(poTokenClass) { + name == "" && + parameters == listOf("[B") + }.addInstruction( + 1, + "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->setDroidGuardPoToken([B)V" + ) + } + + // endregion + findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { name == "SpoofStreamingData" }.replaceInstruction( diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt index 36ae9360b..c120b5d4e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt @@ -197,3 +197,18 @@ internal val hlsCurrentTimeFingerprint = legacyFingerprint( parameters = listOf("Z", "L"), literals = listOf(HLS_CURRENT_TIME_FEATURE_FLAG), ) + +internal val poTokenToStringFingerprint = legacyFingerprint( + name = "poTokenToStringFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("UTF-8"), + customFingerprint = { method, classDef -> + method.name == "toString" && + classDef.fields.find { it.type == "[B" } != null && + // In YouTube, this field's type is 'Lcom/google/android/gms/potokens/PoToken;'. + // In YouTube Music, this class name is obfuscated. + classDef.fields.find { it.accessFlags == AccessFlags.PRIVATE.value && it.type.startsWith("L") } != null + }, +) diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml index 21e7dfcf6..3aaf77a14 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -1930,6 +1930,19 @@ AVC has a maximum resolution of 1080p, Opus audio codec is not available, and vi Client used to fetch streaming data is shown in Stats for nerds. Client used to fetch streaming data is hidden in Stats for nerds. + + PoToken / VisitorData + PoToken to use + PoToken issued by BotGuard in a trusted browser. + VisitorData to use + VisitorData issued by BotGuard in a trusted browser. + About PoToken / VisitorData + "Some clients require PoToken and VisitorData to get a valid streaming data response. + +If you are trying to use iOS as the default client, you may need these values. + +Click to see more information." + Watch history Change settings related with watch history. diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml index 396af8202..15e79d88d 100644 --- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -788,6 +788,12 @@ + + + + + + SETTINGS: SPOOF_STREAMING_DATA -->