Skip to content

Commit 21743d6

Browse files
committed
feat(YouTube - Spoof streaming data): Add setting to change PoToken / Visitor Data inotia00/ReVanced_Extended#2630 (comment)
1 parent 83f2d82 commit 21743d6

File tree

10 files changed

+132
-22
lines changed

10 files changed

+132
-22
lines changed

extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ public enum ClientType {
181181
ANDROID_SDK_VERSION_ANDROID_VR,
182182
CLIENT_VERSION_ANDROID_VR,
183183
true,
184+
false,
184185
"Android VR"
185186
),
186187
ANDROID_UNPLUGGED(29,
@@ -190,6 +191,7 @@ public enum ClientType {
190191
ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
191192
CLIENT_VERSION_ANDROID_UNPLUGGED,
192193
true,
194+
false,
193195
"Android TV"
194196
),
195197
IOS_UNPLUGGED(33,
@@ -199,6 +201,7 @@ public enum ClientType {
199201
null,
200202
CLIENT_VERSION_IOS_UNPLUGGED,
201203
true,
204+
false,
202205
forceAVC()
203206
? "iOS TV Force AVC"
204207
: "iOS TV"
@@ -210,6 +213,7 @@ public enum ClientType {
210213
null,
211214
CLIENT_VERSION_IOS,
212215
false,
216+
true,
213217
forceAVC()
214218
? "iOS Force AVC"
215219
: "iOS"
@@ -222,6 +226,7 @@ public enum ClientType {
222226
null,
223227
CLIENT_VERSION_IOS_MUSIC,
224228
true,
229+
false,
225230
"iOS Music"
226231
);
227232

@@ -265,6 +270,11 @@ public enum ClientType {
265270
*/
266271
public final boolean canLogin;
267272

273+
/**
274+
* If a poToken should be used.
275+
*/
276+
public final boolean usePoToken;
277+
268278
/**
269279
* Friendly name displayed in stats for nerds.
270280
*/
@@ -277,6 +287,7 @@ public enum ClientType {
277287
@Nullable String androidSdkVersion,
278288
String clientVersion,
279289
boolean canLogin,
290+
boolean usePoToken,
280291
String friendlyName
281292
) {
282293
this.id = id;
@@ -287,6 +298,7 @@ public enum ClientType {
287298
this.androidSdkVersion = androidSdkVersion;
288299
this.userAgent = userAgent;
289300
this.canLogin = canLogin;
301+
this.usePoToken = usePoToken;
290302
this.friendlyName = friendlyName;
291303
}
292304

extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
import android.net.Uri;
66
import android.text.TextUtils;
7+
import android.util.Base64;
78

9+
import androidx.annotation.NonNull;
810
import androidx.annotation.Nullable;
911

1012
import java.nio.ByteBuffer;
@@ -19,14 +21,21 @@
1921

2022
@SuppressWarnings("unused")
2123
public class SpoofStreamingDataPatch {
22-
public static final boolean SPOOF_STREAMING_DATA = SpoofStreamingData() && BaseSettings.SPOOF_STREAMING_DATA.get();
24+
private static final boolean SPOOF_STREAMING_DATA = SpoofStreamingData() && BaseSettings.SPOOF_STREAMING_DATA.get();
25+
private static final String PO_TOKEN =
26+
BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get();
27+
private static final String VISITOR_DATA =
28+
BaseSettings.SPOOF_STREAMING_DATA_VISITOR_DATA.get();
2329

2430
/**
2531
* Any unreachable ip address. Used to intentionally fail requests.
2632
*/
2733
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
2834
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
2935

36+
@NonNull
37+
private static volatile String droidGuardPoToken = "";
38+
3039
/**
3140
* Key: video id
3241
* Value: original video length [streamingData.formats.approxDurationMs]
@@ -128,7 +137,7 @@ public static void fetchStreams(String url, Map<String, String> requestHeaders)
128137
return;
129138
}
130139

131-
StreamingDataRequest.fetchRequest(id, requestHeaders);
140+
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN, droidGuardPoToken);
132141
}
133142
} catch (Exception ex) {
134143
Logger.printException(() -> "buildRequest failure", ex);
@@ -253,4 +262,17 @@ public static String appendSpoofedClient(String videoFormat) {
253262

254263
return videoFormat;
255264
}
265+
266+
/**
267+
* Injection point.
268+
*/
269+
public static void setDroidGuardPoToken(byte[] bytes) {
270+
if (SPOOF_STREAMING_DATA && bytes.length > 20) {
271+
final String poToken = Base64.encodeToString(bytes, Base64.URL_SAFE);
272+
if (!droidGuardPoToken.equals(poToken)) {
273+
Logger.printDebug(() -> "New droidGuardPoToken loaded:\n" + poToken);
274+
droidGuardPoToken = poToken;
275+
}
276+
}
277+
}
256278
}

extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,7 @@ public final class PlayerRoutes {
3737
private PlayerRoutes() {
3838
}
3939

40-
public static String createInnertubeBody(ClientType clientType) {
41-
return createInnertubeBody(clientType, false);
42-
}
43-
44-
public static String createInnertubeBody(ClientType clientType, boolean playlistId) {
40+
public static JSONObject createInnertubeBody(ClientType clientType) {
4541
JSONObject innerTubeBody = new JSONObject();
4642

4743
try {
@@ -66,14 +62,11 @@ public static String createInnertubeBody(ClientType clientType, boolean playlist
6662
innerTubeBody.put("contentCheckOk", true);
6763
innerTubeBody.put("racyCheckOk", true);
6864
innerTubeBody.put("videoId", "%s");
69-
if (playlistId) {
70-
innerTubeBody.put("playlistId", "%s");
71-
}
7265
} catch (JSONException e) {
7366
Logger.printException(() -> "Failed to create innerTubeBody", e);
7467
}
7568

76-
return innerTubeBody.toString();
69+
return innerTubeBody;
7770
}
7871

7972
/**

extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ public class StreamingDataRequest {
4444

4545
private static final ClientType[] CLIENT_ORDER_TO_USE;
4646
private static final String AUTHORIZATION_HEADER = "Authorization";
47+
private static final String VISITOR_ID_HEADER = "X-Goog-Visitor-Id";
4748
private static final String[] REQUEST_HEADER_KEYS = {
4849
AUTHORIZATION_HEADER, // Available only to logged-in users.
4950
"X-GOOG-API-FORMAT-VERSION",
50-
"X-Goog-Visitor-Id"
51+
VISITOR_ID_HEADER
5152
};
5253
private static ClientType lastSpoofedClientType;
5354

@@ -105,15 +106,17 @@ public static String getLastSpoofedClientName() {
105106
private final String videoId;
106107
private final Future<ByteBuffer> future;
107108

108-
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
109+
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders, String visitorId,
110+
String botGuardPoToken, String droidGuardPoToken) {
109111
Objects.requireNonNull(playerHeaders);
110112
this.videoId = videoId;
111-
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
113+
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders, visitorId, botGuardPoToken, droidGuardPoToken));
112114
}
113115

114-
public static void fetchRequest(String videoId, Map<String, String> fetchHeaders) {
116+
public static void fetchRequest(String videoId, Map<String, String> fetchHeaders, String droidGuardPoToken,
117+
String botGuardPoToken, String visitorId) {
115118
// Always fetch, even if there is an existing request for the same video.
116-
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
119+
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders, droidGuardPoToken, botGuardPoToken, visitorId));
117120
}
118121

119122
@Nullable
@@ -126,8 +129,8 @@ private static void handleConnectionError(String toastMessage, @Nullable Excepti
126129
}
127130

128131
@Nullable
129-
private static HttpURLConnection send(ClientType clientType, String videoId,
130-
Map<String, String> playerHeaders) {
132+
private static HttpURLConnection send(ClientType clientType, String videoId, Map<String, String> playerHeaders,
133+
String visitorId, String botGuardPoToken, String droidGuardPoToken) {
131134
Objects.requireNonNull(clientType);
132135
Objects.requireNonNull(videoId);
133136
Objects.requireNonNull(playerHeaders);
@@ -149,12 +152,32 @@ private static HttpURLConnection send(ClientType clientType, String videoId,
149152
continue;
150153
}
151154
}
155+
if (key.equals(VISITOR_ID_HEADER) &&
156+
clientType.usePoToken &&
157+
!botGuardPoToken.isEmpty() &&
158+
!visitorId.isEmpty()) {
159+
String originalVisitorId = value;
160+
Logger.printDebug(() -> "Original visitor id:\n" + originalVisitorId);
161+
Logger.printDebug(() -> "Replaced visitor id:\n" + visitorId);
162+
value = visitorId;
163+
}
152164

153165
connection.setRequestProperty(key, value);
154166
}
155167
}
156168

157-
String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
169+
JSONObject innerTubeBodyJson = PlayerRoutes.createInnertubeBody(clientType);
170+
if (clientType.usePoToken && !botGuardPoToken.isEmpty() && !visitorId.isEmpty()) {
171+
JSONObject serviceIntegrityDimensions = new JSONObject();
172+
serviceIntegrityDimensions.put("poToken", botGuardPoToken);
173+
innerTubeBodyJson.put("serviceIntegrityDimensions", serviceIntegrityDimensions);
174+
if (!droidGuardPoToken.isEmpty()) {
175+
Logger.printDebug(() -> "Original poToken (droidGuardPoToken):\n" + droidGuardPoToken);
176+
}
177+
Logger.printDebug(() -> "Replaced poToken (botGuardPoToken):\n" + botGuardPoToken);
178+
}
179+
180+
String innerTubeBody = String.format(innerTubeBodyJson.toString(), videoId);
158181
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
159182
connection.setFixedLengthStreamingMode(requestBody.length);
160183
connection.getOutputStream().write(requestBody);
@@ -180,12 +203,13 @@ private static HttpURLConnection send(ClientType clientType, String videoId,
180203
return null;
181204
}
182205

183-
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
206+
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders, String visitorId,
207+
String botGuardPoToken, String droidGuardPoToken) {
184208
lastSpoofedClientType = null;
185209

186210
// Retry with different client if empty response body is received.
187211
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
188-
HttpURLConnection connection = send(clientType, videoId, playerHeaders);
212+
HttpURLConnection connection = send(clientType, videoId, playerHeaders, visitorId, botGuardPoToken, droidGuardPoToken);
189213
if (connection != null) {
190214
try {
191215
// gzip encoding doesn't response with content length (-1),

extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public class BaseSettings {
4242
// Client type must be last spoof setting due to cyclic references.
4343
public static final EnumSetting<ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true);
4444

45+
public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
46+
public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
47+
4548
/**
4649
* @noinspection DeprecatedIsStillUsed
4750
*/

extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,12 @@ private static JSONObject send(ClientType clientType, String videoId) {
8484
try {
8585
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType);
8686

87+
JSONObject innerTubeBodyJson = PlayerRoutes.createInnertubeBody(clientType);
88+
innerTubeBodyJson.put("playlistId", "%s");
89+
8790
String innerTubeBody = String.format(
8891
Locale.ENGLISH,
89-
PlayerRoutes.createInnertubeBody(clientType, true),
92+
innerTubeBodyJson.toString(),
9093
videoId,
9194
"RD" + videoId
9295
);

patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import app.revanced.util.fingerprint.definingClassOrThrow
2121
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
2222
import app.revanced.util.fingerprint.matchOrThrow
2323
import app.revanced.util.fingerprint.methodOrThrow
24+
import app.revanced.util.fingerprint.mutableClassOrThrow
2425
import app.revanced.util.getReference
2526
import app.revanced.util.indexOfFirstInstructionOrThrow
2627
import com.android.tools.smali.dexlib2.AccessFlags
@@ -362,6 +363,24 @@ fun baseSpoofStreamingDataPatch(
362363

363364
// endregion
364365

366+
// region Set DroidGuard poToken.
367+
368+
poTokenToStringFingerprint.mutableClassOrThrow().let {
369+
val poTokenClass = it.fields.find { field ->
370+
field.accessFlags == AccessFlags.PRIVATE.value && field.type.startsWith("L")
371+
}!!.type
372+
373+
findMethodOrThrow(poTokenClass) {
374+
name == "<init>" &&
375+
parameters == listOf("[B")
376+
}.addInstruction(
377+
1,
378+
"invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->setDroidGuardPoToken([B)V"
379+
)
380+
}
381+
382+
// endregion
383+
365384
findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
366385
name == "SpoofStreamingData"
367386
}.replaceInstruction(

patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,18 @@ internal val hlsCurrentTimeFingerprint = legacyFingerprint(
197197
parameters = listOf("Z", "L"),
198198
literals = listOf(HLS_CURRENT_TIME_FEATURE_FLAG),
199199
)
200+
201+
internal val poTokenToStringFingerprint = legacyFingerprint(
202+
name = "poTokenToStringFingerprint",
203+
returnType = "Ljava/lang/String;",
204+
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
205+
parameters = emptyList(),
206+
strings = listOf("UTF-8"),
207+
customFingerprint = { method, classDef ->
208+
method.name == "toString" &&
209+
classDef.fields.find { it.type == "[B" } != null &&
210+
// In YouTube, this field's type is 'Lcom/google/android/gms/potokens/PoToken;'.
211+
// In YouTube Music, this class name is obfuscated.
212+
classDef.fields.find { it.accessFlags == AccessFlags.PRIVATE.value && it.type.startsWith("L") } != null
213+
},
214+
)

patches/src/main/resources/youtube/settings/host/values/strings.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,19 @@ AVC has a maximum resolution of 1080p, Opus audio codec is not available, and vi
19301930
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_on">Client used to fetch streaming data is shown in Stats for nerds.</string>
19311931
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Client used to fetch streaming data is hidden in Stats for nerds.</string>
19321932

1933+
<!-- PreferenceScreen: Miscellaneous, PreferenceCategory: Miscellaneous, PreferenceScreen: Spoof streaming data, PreferenceCategory: PoToken / VisitorData -->
1934+
<string name="revanced_preference_category_po_token_visitor_data">PoToken / VisitorData</string>
1935+
<string name="revanced_spoof_streaming_data_po_token_title">PoToken to use</string>
1936+
<string name="revanced_spoof_streaming_data_po_token_summary">PoToken issued by BotGuard in a trusted browser.</string>
1937+
<string name="revanced_spoof_streaming_data_visitor_data_title">VisitorData to use</string>
1938+
<string name="revanced_spoof_streaming_data_visitor_data_summary">VisitorData issued by BotGuard in a trusted browser.</string>
1939+
<string name="revanced_spoof_streaming_data_po_token_visitor_data_about_title">About PoToken / VisitorData</string>
1940+
<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.
1941+
1942+
If you are trying to use iOS as the default client, you may need these values.
1943+
1944+
Click to see more information."</string>
1945+
19331946
<!-- PreferenceScreen: Miscellaneous, PreferenceCategory: Miscellaneous, PreferenceScreen: Watch history -->
19341947
<string name="revanced_preference_screen_watch_history_title">Watch history</string>
19351948
<string name="revanced_preference_screen_watch_history_summary">Change settings related with watch history.</string>

patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,12 @@
788788
<app.revanced.extension.youtube.settings.preference.SpoofStreamingDataSideEffectsPreference android:title="@string/revanced_spoof_streaming_data_side_effects_title" />
789789
<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" />
790790
<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" />
791+
<PreferenceCategory android:title="@string/revanced_preference_category_po_token_visitor_data" android:layout="@layout/revanced_settings_preferences_category" />
792+
<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" />
793+
<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" />
794+
<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">
795+
<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" />
796+
</Preference>
791797
</PreferenceScreen>SETTINGS: SPOOF_STREAMING_DATA -->
792798

793799
<!-- SETTINGS: WATCH_HISTORY

0 commit comments

Comments
 (0)