Skip to content

Commit

Permalink
feat(YouTube - Spoof streaming data): Add setting to change `PoToken …
Browse files Browse the repository at this point in the history
  • Loading branch information
inotia00 committed Dec 31, 2024
1 parent 83f2d82 commit 21743d6
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ public enum ClientType {
ANDROID_SDK_VERSION_ANDROID_VR,
CLIENT_VERSION_ANDROID_VR,
true,
false,
"Android VR"
),
ANDROID_UNPLUGGED(29,
Expand All @@ -190,6 +191,7 @@ public enum ClientType {
ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
CLIENT_VERSION_ANDROID_UNPLUGGED,
true,
false,
"Android TV"
),
IOS_UNPLUGGED(33,
Expand All @@ -199,6 +201,7 @@ public enum ClientType {
null,
CLIENT_VERSION_IOS_UNPLUGGED,
true,
false,
forceAVC()
? "iOS TV Force AVC"
: "iOS TV"
Expand All @@ -210,6 +213,7 @@ public enum ClientType {
null,
CLIENT_VERSION_IOS,
false,
true,
forceAVC()
? "iOS Force AVC"
: "iOS"
Expand All @@ -222,6 +226,7 @@ public enum ClientType {
null,
CLIENT_VERSION_IOS_MUSIC,
true,
false,
"iOS Music"
);

Expand Down Expand Up @@ -265,6 +270,11 @@ public enum ClientType {
*/
public final boolean canLogin;

/**
* If a poToken should be used.
*/
public final boolean usePoToken;

/**
* Friendly name displayed in stats for nerds.
*/
Expand All @@ -277,6 +287,7 @@ public enum ClientType {
@Nullable String androidSdkVersion,
String clientVersion,
boolean canLogin,
boolean usePoToken,
String friendlyName
) {
this.id = id;
Expand All @@ -287,6 +298,7 @@ public enum ClientType {
this.androidSdkVersion = androidSdkVersion;
this.userAgent = userAgent;
this.canLogin = canLogin;
this.usePoToken = usePoToken;
this.friendlyName = friendlyName;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import android.net.Uri;
import android.text.TextUtils;
import android.util.Base64;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.nio.ByteBuffer;
Expand All @@ -19,14 +21,21 @@

@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.
*/
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]
Expand Down Expand Up @@ -128,7 +137,7 @@ public static void fetchStreams(String url, Map<String, String> requestHeaders)
return;
}

StreamingDataRequest.fetchRequest(id, requestHeaders);
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN, droidGuardPoToken);
}
} catch (Exception ex) {
Logger.printException(() -> "buildRequest failure", ex);
Expand Down Expand Up @@ -253,4 +262,17 @@ public static String appendSpoofedClient(String videoFormat) {

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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -66,14 +62,11 @@ public static String createInnertubeBody(ClientType clientType, boolean playlist
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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -105,15 +106,17 @@ public static String getLastSpoofedClientName() {
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 droidGuardPoToken,
String botGuardPoToken, String visitorId) {
// 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, droidGuardPoToken, botGuardPoToken, visitorId));
}

@Nullable
Expand All @@ -126,8 +129,8 @@ private static void handleConnectionError(String toastMessage, @Nullable Excepti
}

@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);
Expand All @@ -149,12 +152,32 @@ private static HttpURLConnection send(ClientType clientType, String videoId,
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);
Expand All @@ -180,12 +203,13 @@ private static HttpURLConnection send(ClientType clientType, String videoId,
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,12 @@ private static JSONObject send(ClientType clientType, String videoId) {
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
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 21743d6

Please sign in to comment.