mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-05-28 12:50:19 +02:00
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:
parent
83f2d82d0a
commit
00fbd318e7
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
},
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user