From baf967e12ae64f617c59a5d51af3a3982a140d5d Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 26 May 2024 02:10:09 +0400 Subject: [PATCH] fix(YouTube - Spoof client): Improve Android spoofing (#641) --- .../patches/spoof/SpoofClientPatch.java | 173 +----------------- .../patches/spoof/StoryboardRenderer.java | 1 + .../youtube/settings/Settings.java | 2 +- 3 files changed, 11 insertions(+), 165 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java index 021f7a3a..fd1c8135 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofClientPatch.java @@ -1,30 +1,14 @@ package app.revanced.integrations.youtube.patches.spoof; -import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer; - import android.net.Uri; -import android.os.Build; -import androidx.annotation.Nullable; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.youtube.patches.VideoInformation; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public class SpoofClientPatch { private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get(); - private static final boolean SPOOF_CLIENT_USE_TEST_SUITE = Settings.SPOOF_CLIENT_USE_TESTSUITE.get(); - private static final boolean SPOOF_CLIENT_STORYBOARD = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_USE_TEST_SUITE; + private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_USE_IOS.get() ? ClientType.IOS : ClientType.ANDROID_VR; /** * Any unreachable ip address. Used to intentionally fail requests. @@ -32,19 +16,6 @@ public class SpoofClientPatch { 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); - @Nullable - private static volatile Future lastStoryboardFetched; - - private static final Map> storyboardCache = - Collections.synchronizedMap(new LinkedHashMap<>(100) { - private static final int CACHE_LIMIT = 100; - - @Override - protected boolean removeEldestEntry(Entry eldest) { - return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. - } - }); - /** * Injection point. * Blocks /get_watch requests by returning an unreachable URI. @@ -72,8 +43,8 @@ public class SpoofClientPatch { /** * Injection point. + * * Blocks /initplayback requests. - * For iOS, an unreachable host URL can be used, but for Android Testsuite, this is not possible. */ public static String blockInitPlaybackRequest(String originalUrlString) { if (SPOOF_CLIENT_ENABLED) { @@ -82,17 +53,9 @@ public class SpoofClientPatch { String path = originalUri.getPath(); if (path != null && path.contains("initplayback")) { - String replacementUriString = (getSpoofClientType() == ClientType.IOS) - ? UNREACHABLE_HOST_URI_STRING - // TODO: Ideally, a local proxy could be setup and block - // the request the same way as Burp Suite is capable of - // because that way the request is never sent to YouTube unnecessarily. - // Just using localhost unfortunately does not work. - : originalUri.buildUpon().clearQuery().build().toString(); + Logger.printDebug(() -> "Blocking: " + originalUrlString + " by returning unreachable url"); - Logger.printDebug(() -> "Blocking: " + originalUrlString + " by returning: " + replacementUriString); - - return replacementUriString; + return UNREACHABLE_HOST_URI_STRING; } } catch (Exception ex) { Logger.printException(() -> "blockInitPlaybackRequest failure", ex); @@ -102,36 +65,12 @@ public class SpoofClientPatch { return originalUrlString; } - private static ClientType getSpoofClientType() { - if (!SPOOF_CLIENT_USE_TEST_SUITE) { - return ClientType.IOS; - } - - StoryboardRenderer renderer = getRenderer(false); - if (renderer == null) { - // Video is private or otherwise not available. - // Test client still works for video playback, but seekbar thumbnails are not available. - // Use iOS client instead. - Logger.printDebug(() -> "Using iOS client for paid or otherwise restricted video"); - return ClientType.IOS; - } - - if (renderer.isLiveStream) { - // Test client does not support live streams. - // Use the storyboard renderer information to fallback to iOS if a live stream is opened. - Logger.printDebug(() -> "Using iOS client for livestream: " + renderer.videoId); - return ClientType.IOS; - } - - return ClientType.ANDROID_TESTSUITE; - } - /** * Injection point. */ public static int getClientTypeId(int originalClientTypeId) { if (SPOOF_CLIENT_ENABLED) { - return getSpoofClientType().id; + return SPOOF_CLIENT_TYPE.id; } return originalClientTypeId; @@ -142,7 +81,7 @@ public class SpoofClientPatch { */ public static String getClientVersion(String originalClientVersion) { if (SPOOF_CLIENT_ENABLED) { - return getSpoofClientType().version; + return SPOOF_CLIENT_TYPE.version; } return originalClientVersion; @@ -153,7 +92,7 @@ public class SpoofClientPatch { */ public static String getClientModel(String originalClientModel) { if (SPOOF_CLIENT_ENABLED) { - return getSpoofClientType().model; + return SPOOF_CLIENT_TYPE.model; } return originalClientModel; @@ -166,103 +105,9 @@ public class SpoofClientPatch { return SPOOF_CLIENT_ENABLED; } - // - // Storyboard. - // - - /** - * Injection point. - */ - public static String setPlayerResponseVideoId(String parameters, String videoId, boolean isShortAndOpeningOrPlaying) { - if (SPOOF_CLIENT_STORYBOARD) { - try { - // VideoInformation is not a dependent patch, and only this single helper method is used. - // Hook can be called when scrolling thru the feed and a Shorts shelf is present. - // Ignore these videos. - if (!isShortAndOpeningOrPlaying && VideoInformation.playerParametersAreShort(parameters)) { - Logger.printDebug(() -> "Ignoring Short: " + videoId); - return parameters; - } - - Future storyboard = storyboardCache.get(videoId); - if (storyboard == null) { - storyboard = Utils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId)); - storyboardCache.put(videoId, storyboard); - lastStoryboardFetched = storyboard; - - // Block until the renderer fetch completes. - // This is desired because if this returns without finishing the fetch - // then video will start playback but the storyboard is not ready yet. - getRenderer(true); - } else { - lastStoryboardFetched = storyboard; - // No need to block on the fetch since it previously loaded. - } - - } catch (Exception ex) { - Logger.printException(() -> "setPlayerResponseVideoId failure", ex); - } - } - - return parameters; // Return the original value since we are observing and not modifying. - } - - @Nullable - private static StoryboardRenderer getRenderer(boolean waitForCompletion) { - var future = lastStoryboardFetched; - if (future != null) { - try { - if (waitForCompletion || future.isDone()) { - return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout. - } // else, return null. - } catch (TimeoutException ex) { - Logger.printDebug(() -> "Could not get renderer (get timed out)"); - } catch (ExecutionException | InterruptedException ex) { - // Should never happen. - Logger.printException(() -> "Could not get renderer", ex); - } - } - return null; - } - - /** - * Injection point. - * Called from background threads and from the main thread. - */ - @Nullable - public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) { - if (SPOOF_CLIENT_STORYBOARD) { - StoryboardRenderer renderer = getRenderer(false); - - if (renderer != null) { - if (!renderer.isLiveStream && renderer.spec != null) { - return renderer.spec; - } - } - } - - return originalStoryboardRendererSpec; - } - - /** - * Injection point. - */ - public static int getRecommendedLevel(int originalLevel) { - if (SPOOF_CLIENT_STORYBOARD) { - StoryboardRenderer renderer = getRenderer(false); - - if (renderer != null) { - if (!renderer.isLiveStream && renderer.recommendedLevel != null) { - return renderer.recommendedLevel; - } - } - } - - return originalLevel; - } - private enum ClientType { - ANDROID_TESTSUITE(30, Build.MODEL, "1.9"), + // https://dumps.tadiphone.dev/dumps/oculus/monterey/-/blob/vr_monterey-user-7.1.1-NGI77B-256550.6810.0-release-keys/system/system/build.prop?ref_type=heads + ANDROID_VR(28, "Quest", "1.37"), // 16,2 = iPhone 15 Pro Max. // Version number should be a valid iOS release. // https://www.ipa4fun.com/history/185230 diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java index 45bb86eb..5014a5fc 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/StoryboardRenderer.java @@ -4,6 +4,7 @@ import androidx.annotation.Nullable; import org.jetbrains.annotations.NotNull; +@Deprecated public final class StoryboardRenderer { public final String videoId; @Nullable diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index efd05dda..f82450c5 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -237,7 +237,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true, "revanced_spoof_client_user_dialog_message"); - public static final BooleanSetting SPOOF_CLIENT_USE_TESTSUITE = new BooleanSetting("revanced_spoof_client_use_testsuite", FALSE, true, parent(SPOOF_CLIENT)); + public static final BooleanSetting SPOOF_CLIENT_USE_IOS = new BooleanSetting("revanced_spoof_client_use_ios", TRUE, true, parent(SPOOF_CLIENT)); @Deprecated public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", ""); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1);