diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java index b2cc87163..499ac6d74 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -27,6 +27,7 @@ import net.kdt.pojavlaunch.extra.ExtraListener; import net.kdt.pojavlaunch.fragments.MainMenuFragment; import net.kdt.pojavlaunch.fragments.MicrosoftLoginFragment; import net.kdt.pojavlaunch.fragments.SelectAuthFragment; +import net.kdt.pojavlaunch.mirrors.DownloadMirror; import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker; import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.IconCacheJanitor; import net.kdt.pojavlaunch.prefs.LauncherPreferences; @@ -141,6 +142,7 @@ public class LauncherActivity extends BaseActivity { @Override public void onDownloadFailed(Throwable th) { + if(DownloadMirror.checkForTamperedException(LauncherActivity.this, th)) return; if(th != null) Tools.showError(LauncherActivity.this, R.string.mc_download_failed, th); } }); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModVersionListFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModVersionListFragment.java index 581e487f0..b8150987e 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModVersionListFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModVersionListFragment.java @@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.mirrors.DownloadMirror; import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener; import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; @@ -143,6 +144,7 @@ public abstract class ModVersionListFragment extends Fragment implements Runn getTaskProxy().detachListener(); setTaskProxy(null); mExpandableListView.setEnabled(true); + if(DownloadMirror.checkForTamperedException(context, e)) return; Tools.showError(context, e); }); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java new file mode 100644 index 000000000..369cd1108 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java @@ -0,0 +1,126 @@ +package net.kdt.pojavlaunch.mirrors; + +import android.app.AlertDialog; +import android.content.Context; +import android.text.Html; +import android.util.Log; + +import androidx.annotation.Nullable; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.prefs.LauncherPreferences; +import net.kdt.pojavlaunch.utils.DownloadUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; + +public class DownloadMirror { + public static final int DOWNLOAD_CLASS_LIBRARIES = 0; + public static final int DOWNLOAD_CLASS_METADATA = 1; + public static final int DOWNLOAD_CLASS_ASSETS = 2; + + private static final String[] MIRROR_BMCLAPI = { + "https://bmclapi2.bangbang93.com/maven", + "https://bmclapi2.bangbang93.com", + "https://bmclapi2.bangbang93.com/assets" + }; + + private static final String[] MIRROR_MCBBS = { + "https://download.mcbbs.net/maven", + "https://download.mcbbs.net", + "https://download.mcbbs.net/assets" + }; + + /** + * Download a file with the current mirror. If the file is missing on the mirror, + * fall back to the official source. + * @param downloadClass Class of the download. Can either be DOWNLOAD_CLASS_LIBRARIES, + * DOWNLOAD_CLASS_METADATA or DOWNLOAD_CLASS_ASSETS + * @param urlInput The original (Mojang) URL for the download + * @param outputFile The output file for the download + * @param buffer The shared buffer + * @param monitor The download monitor. + */ + public static void downloadFileMirrored(int downloadClass, String urlInput, File outputFile, + @Nullable byte[] buffer, Tools.DownloaderFeedback monitor) throws IOException { + try { + DownloadUtils.downloadFileMonitored(getMirrorMapping(downloadClass, urlInput), + outputFile, buffer, monitor); + return; + }catch (FileNotFoundException e) { + Log.w("DownloadMirror", "Cannot find the file on the mirror", e); + Log.i("DownloadMirror", "Failling back to default source"); + } + DownloadUtils.downloadFileMonitored(urlInput, outputFile, buffer, monitor); + } + + public static boolean isMirrored() { + return !LauncherPreferences.PREF_DOWNLOAD_SOURCE.equals("default"); + } + + public static boolean checkForTamperedException(Context context, Throwable e) { + if(e instanceof MirrorTamperedException){ + showMirrorTamperedDialog(context); + return true; + } + return false; + } + + private static void showMirrorTamperedDialog(Context ctx) { + Tools.runOnUiThread(()->{ + AlertDialog.Builder builder = new AlertDialog.Builder(ctx); + builder.setTitle(R.string.dl_tampered_manifest_title); + builder.setMessage(Html.fromHtml(ctx.getString(R.string.dl_tampered_manifest))); + builder.setPositiveButton(R.string.dl_switch_to_official_site,(d,w)->{ + LauncherPreferences.DEFAULT_PREF.edit().putString("downloadSource", "default").apply(); + LauncherPreferences.PREF_DOWNLOAD_SOURCE = "default"; + + }); + builder.setNegativeButton(R.string.dl_turn_off_manifest_checks,(d,w)->{ + LauncherPreferences.DEFAULT_PREF.edit().putBoolean("verifyManifest", false).apply(); + LauncherPreferences.PREF_VERIFY_MANIFEST = false; + }); + builder.setNeutralButton(android.R.string.cancel, (d,w)->{}); + builder.show(); + }); + } + + private static String[] getMirrorSettings() { + switch (LauncherPreferences.PREF_DOWNLOAD_SOURCE) { + case "mcbbs": return MIRROR_MCBBS; + case "bmclapi": return MIRROR_BMCLAPI; + case "default": + default: + return null; + } + } + + private static String getMirrorMapping(int downloadClass, String mojangUrl) throws MalformedURLException{ + String[] mirrorSettings = getMirrorSettings(); + if(mirrorSettings == null) return mojangUrl; + int urlTail = getBaseUrlTail(mojangUrl); + String baseUrl = mojangUrl.substring(0, urlTail); + String path = mojangUrl.substring(urlTail); + switch(downloadClass) { + case DOWNLOAD_CLASS_ASSETS: + case DOWNLOAD_CLASS_METADATA: + baseUrl = mirrorSettings[downloadClass]; + break; + case DOWNLOAD_CLASS_LIBRARIES: + if(!baseUrl.endsWith("libraries.minecraft.net")) break; + baseUrl = mirrorSettings[downloadClass]; + break; + } + return baseUrl + path; + } + + private static int getBaseUrlTail(String wholeUrl) throws MalformedURLException{ + int protocolNameEnd = wholeUrl.indexOf("://"); + if(protocolNameEnd == -1) throw new MalformedURLException("No protocol"); + protocolNameEnd += 3; + return wholeUrl.indexOf('/', protocolNameEnd); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/MirrorTamperedException.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/MirrorTamperedException.java new file mode 100644 index 000000000..548d414e1 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/MirrorTamperedException.java @@ -0,0 +1,4 @@ +package net.kdt.pojavlaunch.mirrors; + +public class MirrorTamperedException extends Exception{ +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java index ed7a42ad6..531073d9d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java @@ -61,6 +61,9 @@ public class LauncherPreferences { public static float PREF_DEADZONE_SCALE = 1f; public static boolean PREF_BIG_CORE_AFFINITY = false; public static boolean PREF_ZINK_PREFER_SYSTEM_DRIVER = false; + + public static boolean PREF_VERIFY_MANIFEST = true; + public static String PREF_DOWNLOAD_SOURCE = "default"; @@ -104,6 +107,8 @@ public class LauncherPreferences { PREF_DEADZONE_SCALE = DEFAULT_PREF.getInt("gamepad_deadzone_scale", 100)/100f; PREF_BIG_CORE_AFFINITY = DEFAULT_PREF.getBoolean("bigCoreAffinity", false); PREF_ZINK_PREFER_SYSTEM_DRIVER = DEFAULT_PREF.getBoolean("zinkPreferSystemDriver", false); + PREF_DOWNLOAD_SOURCE = DEFAULT_PREF.getString("downloadSource", "default"); + PREF_VERIFY_MANIFEST = DEFAULT_PREF.getBoolean("verifyManifest", true); String argLwjglLibname = "-Dorg.lwjgl.opengl.libname="; for (String arg : JREUtils.parseJavaArguments(PREF_CUSTOM_JAVA_ARGS)) { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java index 559a81bf5..def624f51 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java @@ -2,7 +2,7 @@ package net.kdt.pojavlaunch.tasks; import static net.kdt.pojavlaunch.PojavApplication.sExecutorService; import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB; -import static net.kdt.pojavlaunch.utils.DownloadUtils.downloadFileMonitored; +import static net.kdt.pojavlaunch.mirrors.DownloadMirror.downloadFileMirrored; import android.app.Activity; import android.util.Log; @@ -19,9 +19,10 @@ import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.extra.ExtraConstants; import net.kdt.pojavlaunch.extra.ExtraCore; +import net.kdt.pojavlaunch.mirrors.DownloadMirror; +import net.kdt.pojavlaunch.mirrors.MirrorTamperedException; import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; -import net.kdt.pojavlaunch.utils.DownloadUtils; import net.kdt.pojavlaunch.value.DependentLibrary; import net.kdt.pojavlaunch.value.MinecraftClientInfo; import net.kdt.pojavlaunch.value.MinecraftLibraryArtifact; @@ -127,7 +128,8 @@ public class AsyncMinecraftDownloader { if (!outLib.exists()) { ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.mcl_launch_downloading, verInfo.logging.client.file.id); JMinecraftVersionList.Version finalVerInfo = verInfo; - downloadFileMonitored( + downloadFileMirrored( + DownloadMirror.DOWNLOAD_CLASS_METADATA, verInfo.logging.client.file.url, outLib, getByteBuffer(), (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, finalVerInfo.logging.client.file.id, curr/BYTE_TO_MB, max/BYTE_TO_MB) @@ -185,6 +187,7 @@ public class AsyncMinecraftDownloader { } } } catch (DownloaderException e) { + ProgressKeeper.submitProgress(ProgressLayout.DOWNLOAD_MINECRAFT, -1, -1); throw e; } catch (Throwable e) { Log.e("AsyncMcDownloader", e.toString(),e ); @@ -214,7 +217,7 @@ public class AsyncMinecraftDownloader { } public void verifyAndDownloadMainJar(String url, String sha1, File destination) throws Exception{ - while(!destination.exists() || (destination.exists() && !Tools.compareSHA1(destination, sha1))) downloadFileMonitored( + while(!destination.exists() || (destination.exists() && !Tools.compareSHA1(destination, sha1))) downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_LIBRARIES, url, destination, getByteBuffer(), (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, @@ -289,7 +292,7 @@ public class AsyncMinecraftDownloader { public void downloadAsset(JAssetInfo asset, File objectsDir, AtomicInteger downloadCounter) throws IOException { String assetPath = asset.hash.substring(0, 2) + "/" + asset.hash; File outFile = new File(objectsDir, assetPath); - downloadFileMonitored(MINECRAFT_RES + assetPath, outFile, getByteBuffer(), + downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_ASSETS, MINECRAFT_RES + assetPath, outFile, getByteBuffer(), new Tools.DownloaderFeedback() { int prevCurr; @Override @@ -303,7 +306,7 @@ public class AsyncMinecraftDownloader { public void downloadAssetMapped(JAssetInfo asset, String assetName, File resDir, AtomicInteger downloadCounter) throws IOException { String assetPath = asset.hash.substring(0, 2) + "/" + asset.hash; File outFile = new File(resDir,"/"+assetName); - downloadFileMonitored(MINECRAFT_RES + assetPath, outFile, getByteBuffer(), + downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_ASSETS, MINECRAFT_RES + assetPath, outFile, getByteBuffer(), new Tools.DownloaderFeedback() { int prevCurr; @Override @@ -336,7 +339,7 @@ public class AsyncMinecraftDownloader { timesChecked++; if(timesChecked > 5) throw new RuntimeException("Library download failed after 5 retries"); - downloadFileMonitored(libPathURL, outLib, getByteBuffer(), + downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_LIBRARIES, libPathURL, outLib, getByteBuffer(), (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, outLib.getName(), curr/BYTE_TO_MB, max/BYTE_TO_MB) ); @@ -362,30 +365,44 @@ public class AsyncMinecraftDownloader { public JAssets downloadIndex(JMinecraftVersionList.Version version, File output) throws IOException { if (!output.exists()) { output.getParentFile().mkdirs(); - DownloadUtils.downloadFile(version.assetIndex != null + downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_METADATA, version.assetIndex != null ? version.assetIndex.url - : "https://s3.amazonaws.com/Minecraft.Download/indexes/" + version.assets + ".json", output); + : "https://s3.amazonaws.com/Minecraft.Download/indexes/" + version.assets + ".json", output, null, + (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, + (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, output.getName(), curr/BYTE_TO_MB, max/BYTE_TO_MB) + ); } return Tools.GLOBAL_GSON.fromJson(Tools.read(output.getAbsolutePath()), JAssets.class); } - public void downloadVersionJson(String versionName, File verJsonDir, JMinecraftVersionList.Version verInfo) throws IOException { + public void downloadVersionJson(String versionName, File verJsonDir, JMinecraftVersionList.Version verInfo) throws IOException, DownloaderException { if(!LauncherPreferences.PREF_CHECK_LIBRARY_SHA) Log.w("Chk","Checker is off"); - boolean isManifestGood = verJsonDir.exists() - && (!LauncherPreferences.PREF_CHECK_LIBRARY_SHA - || verInfo.sha1 == null - || Tools.compareSHA1(verJsonDir, verInfo.sha1)); - - if(!isManifestGood) { + boolean isManifestGood = verifyManifest(verJsonDir, verInfo); + byte retryCount = 0; + while(!isManifestGood && retryCount < 5) { ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.mcl_launch_downloading, versionName + ".json"); - verJsonDir.delete(); - downloadFileMonitored(verInfo.url, verJsonDir, getByteBuffer(), + downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_METADATA, verInfo.url, verJsonDir, getByteBuffer(), (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, versionName + ".json", curr/BYTE_TO_MB, max/BYTE_TO_MB) ); + isManifestGood = verifyManifest(verJsonDir, verInfo); + retryCount++; + // Always do the first verification. But skip all the errors from further ones. + if(!LauncherPreferences.PREF_VERIFY_MANIFEST) return; } + if(!isManifestGood) { + if(DownloadMirror.isMirrored()) throw new DownloaderException(new MirrorTamperedException()); + else throw new DownloaderException(new IOException("Manifest check failed after 5 tries")); + } + } + + private boolean verifyManifest(File verJsonDir, JMinecraftVersionList.Version verInfo) { + return verJsonDir.exists() + && (!LauncherPreferences.PREF_CHECK_LIBRARY_SHA + || verInfo.sha1 == null + || Tools.compareSHA1(verJsonDir, verInfo.sha1)); } public static String normalizeVersionId(String versionString) { @@ -432,5 +449,4 @@ public class AsyncMinecraftDownloader { super(e); } } - } diff --git a/app_pojavlauncher/src/main/res/values/headings_array.xml b/app_pojavlauncher/src/main/res/values/headings_array.xml index f1ae79ee9..4aeec2565 100644 --- a/app_pojavlauncher/src/main/res/values/headings_array.xml +++ b/app_pojavlauncher/src/main/res/values/headings_array.xml @@ -40,5 +40,15 @@ vulkan_zink opengles3_desktopgl_angle_vulkan + + @string/global_default + BMCLAPI + MCBBS + + + default + bmclapi + mcbbs + diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index 422cc238c..8e8c0bf31 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -338,4 +338,16 @@ An error has occurred Click to see more details Show only stable versions + File verification warning + + This is not safe. Your personal information may be at risk if you continue.
+ If you still want to use the mirror, press \"Turn off manifest checks\" and start the download again.
+ If you want to use the official download source, press \"Switch to official site\" and start the download again.
]]> +
+ Turn off manifest checks + Switch to official site + Game download source + Select a download mirror instead of using the official download server + Verify game version manifest + When enabled, the launcher will check the game version manifest along with the libraries. diff --git a/app_pojavlauncher/src/main/res/xml/pref_misc.xml b/app_pojavlauncher/src/main/res/xml/pref_misc.xml index b93694ab6..fbb1f7b4f 100644 --- a/app_pojavlauncher/src/main/res/xml/pref_misc.xml +++ b/app_pojavlauncher/src/main/res/xml/pref_misc.xml @@ -16,6 +16,20 @@ android:key="arc_capes" android:summary="@string/arc_capes_desc" android:title="@string/arc_capes_title" /> + + +