mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-04-29 22:24:34 +02:00
fix(YouTube - Spoof client): Improve Android spoofing (#641)
This commit is contained in:
parent
702df1a93d
commit
baf967e12a
@ -1,30 +1,14 @@
|
|||||||
package app.revanced.integrations.youtube.patches.spoof;
|
package app.revanced.integrations.youtube.patches.spoof;
|
||||||
|
|
||||||
import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
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.Logger;
|
||||||
import app.revanced.integrations.shared.Utils;
|
|
||||||
import app.revanced.integrations.youtube.patches.VideoInformation;
|
|
||||||
import app.revanced.integrations.youtube.settings.Settings;
|
import app.revanced.integrations.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofClientPatch {
|
public class SpoofClientPatch {
|
||||||
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
|
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 ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_USE_IOS.get() ? ClientType.IOS : ClientType.ANDROID_VR;
|
||||||
private static final boolean SPOOF_CLIENT_STORYBOARD = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_USE_TEST_SUITE;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any unreachable ip address. Used to intentionally fail requests.
|
* 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 String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
|
||||||
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
|
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static volatile Future<StoryboardRenderer> lastStoryboardFetched;
|
|
||||||
|
|
||||||
private static final Map<String, Future<StoryboardRenderer>> 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.
|
* Injection point.
|
||||||
* Blocks /get_watch requests by returning an unreachable URI.
|
* Blocks /get_watch requests by returning an unreachable URI.
|
||||||
@ -72,8 +43,8 @@ public class SpoofClientPatch {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
|
*
|
||||||
* Blocks /initplayback requests.
|
* 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) {
|
public static String blockInitPlaybackRequest(String originalUrlString) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
@ -82,17 +53,9 @@ public class SpoofClientPatch {
|
|||||||
String path = originalUri.getPath();
|
String path = originalUri.getPath();
|
||||||
|
|
||||||
if (path != null && path.contains("initplayback")) {
|
if (path != null && path.contains("initplayback")) {
|
||||||
String replacementUriString = (getSpoofClientType() == ClientType.IOS)
|
Logger.printDebug(() -> "Blocking: " + originalUrlString + " by returning unreachable url");
|
||||||
? 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: " + replacementUriString);
|
return UNREACHABLE_HOST_URI_STRING;
|
||||||
|
|
||||||
return replacementUriString;
|
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
|
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
|
||||||
@ -102,36 +65,12 @@ public class SpoofClientPatch {
|
|||||||
return originalUrlString;
|
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.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static int getClientTypeId(int originalClientTypeId) {
|
public static int getClientTypeId(int originalClientTypeId) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
return getSpoofClientType().id;
|
return SPOOF_CLIENT_TYPE.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalClientTypeId;
|
return originalClientTypeId;
|
||||||
@ -142,7 +81,7 @@ public class SpoofClientPatch {
|
|||||||
*/
|
*/
|
||||||
public static String getClientVersion(String originalClientVersion) {
|
public static String getClientVersion(String originalClientVersion) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
return getSpoofClientType().version;
|
return SPOOF_CLIENT_TYPE.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalClientVersion;
|
return originalClientVersion;
|
||||||
@ -153,7 +92,7 @@ public class SpoofClientPatch {
|
|||||||
*/
|
*/
|
||||||
public static String getClientModel(String originalClientModel) {
|
public static String getClientModel(String originalClientModel) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
return getSpoofClientType().model;
|
return SPOOF_CLIENT_TYPE.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalClientModel;
|
return originalClientModel;
|
||||||
@ -166,103 +105,9 @@ public class SpoofClientPatch {
|
|||||||
return SPOOF_CLIENT_ENABLED;
|
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<StoryboardRenderer> 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 {
|
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.
|
// 16,2 = iPhone 15 Pro Max.
|
||||||
// Version number should be a valid iOS release.
|
// Version number should be a valid iOS release.
|
||||||
// https://www.ipa4fun.com/history/185230
|
// https://www.ipa4fun.com/history/185230
|
||||||
|
@ -4,6 +4,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public final class StoryboardRenderer {
|
public final class StoryboardRenderer {
|
||||||
public final String videoId;
|
public final String videoId;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -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 BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
||||||
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", 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 = 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
|
@Deprecated
|
||||||
public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", "");
|
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);
|
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user