feat(YouTube - Spoof streaming data): Add setting to change PoToken / Visitor Data https://github.com/inotia00/ReVanced_Extended/issues/2630#issuecomment-2566310025

This commit is contained in:
inotia00 2024-12-31 21:17:51 +09:00
parent 83f2d82d0a
commit 00fbd318e7
10 changed files with 132 additions and 22 deletions

View File

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

View File

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

View File

@ -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;
}
/**

View File

@ -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<ByteBuffer> future;
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
private StreamingDataRequest(String videoId, Map<String, String> 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<String, String> fetchHeaders) {
public static void fetchRequest(String videoId, Map<String, String> 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<String, String> playerHeaders) {
private static HttpURLConnection send(ClientType clientType, String videoId, Map<String, String> 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<String, String> playerHeaders) {
private static ByteBuffer fetch(String videoId, Map<String, String> 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),

View File

@ -42,6 +42,9 @@ public class BaseSettings {
// Client type must be last spoof setting due to cyclic references.
public static final EnumSetting<ClientType> 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
*/

View File

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

View File

@ -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 == "<init>" &&
parameters == listOf("[B")
}.addInstruction(
1,
"invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->setDroidGuardPoToken([B)V"
)
}
// endregion
findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
name == "SpoofStreamingData"
}.replaceInstruction(

View File

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

View File

@ -1930,6 +1930,19 @@ AVC has a maximum resolution of 1080p, Opus audio codec is not available, and vi
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_on">Client used to fetch streaming data is shown in Stats for nerds.</string>
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Client used to fetch streaming data is hidden in Stats for nerds.</string>
<!-- PreferenceScreen: Miscellaneous, PreferenceCategory: Miscellaneous, PreferenceScreen: Spoof streaming data, PreferenceCategory: PoToken / VisitorData -->
<string name="revanced_preference_category_po_token_visitor_data">PoToken / VisitorData</string>
<string name="revanced_spoof_streaming_data_po_token_title">PoToken to use</string>
<string name="revanced_spoof_streaming_data_po_token_summary">PoToken issued by BotGuard in a trusted browser.</string>
<string name="revanced_spoof_streaming_data_visitor_data_title">VisitorData to use</string>
<string name="revanced_spoof_streaming_data_visitor_data_summary">VisitorData issued by BotGuard in a trusted browser.</string>
<string name="revanced_spoof_streaming_data_po_token_visitor_data_about_title">About PoToken / VisitorData</string>
<string name="revanced_spoof_streaming_data_po_token_visitor_data_about_summary">"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."</string>
<!-- PreferenceScreen: Miscellaneous, PreferenceCategory: Miscellaneous, PreferenceScreen: Watch history -->
<string name="revanced_preference_screen_watch_history_title">Watch history</string>
<string name="revanced_preference_screen_watch_history_summary">Change settings related with watch history.</string>

View File

@ -788,6 +788,12 @@
<app.revanced.extension.youtube.settings.preference.SpoofStreamingDataSideEffectsPreference android:title="@string/revanced_spoof_streaming_data_side_effects_title" />
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_ios_force_avc_title" android:key="revanced_spoof_streaming_data_ios_force_avc" android:summaryOn="@string/revanced_spoof_streaming_data_ios_force_avc_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_ios_force_avc_summary_off" android:dependency="revanced_spoof_streaming_data" />
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_stats_for_nerds_title" android:key="revanced_spoof_streaming_data_stats_for_nerds" android:summaryOn="@string/revanced_spoof_streaming_data_stats_for_nerds_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_stats_for_nerds_summary_off" android:dependency="revanced_spoof_streaming_data" />
<PreferenceCategory android:title="@string/revanced_preference_category_po_token_visitor_data" android:layout="@layout/revanced_settings_preferences_category" />
<app.revanced.extension.shared.settings.preference.ResettableEditTextPreference android:title="@string/revanced_spoof_streaming_data_po_token_title" android:key="revanced_spoof_streaming_data_po_token" android:summary="@string/revanced_spoof_streaming_data_po_token_summary" android:inputType="text" android:dependency="revanced_spoof_streaming_data" />
<app.revanced.extension.shared.settings.preference.ResettableEditTextPreference android:title="@string/revanced_spoof_streaming_data_visitor_data_title" android:key="revanced_spoof_streaming_data_visitor_data" android:summary="@string/revanced_spoof_streaming_data_visitor_data_summary" android:inputType="text" android:dependency="revanced_spoof_streaming_data" />
<Preference android:title="@string/revanced_spoof_streaming_data_po_token_visitor_data_about_title" android:summary="@string/revanced_spoof_streaming_data_po_token_visitor_data_about_summary" android:dependency="revanced_spoof_streaming_data">
<intent android:action="android.intent.action.VIEW" android:data="https://github.com/iv-org/youtube-trusted-session-generator?tab=readme-ov-file#youtube-trusted-session-generator" />
</Preference>
</PreferenceScreen>SETTINGS: SPOOF_STREAMING_DATA -->
<!-- SETTINGS: WATCH_HISTORY