Feat[downloader]: add external mirror support

This commit is contained in:
artdeell
2023-10-04 23:24:51 +03:00
parent d8ed111b10
commit 2b8a3c8ab5
9 changed files with 210 additions and 19 deletions

View File

@@ -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);
}
});

View File

@@ -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<T> extends Fragment implements Runn
getTaskProxy().detachListener();
setTaskProxy(null);
mExpandableListView.setEnabled(true);
if(DownloadMirror.checkForTamperedException(context, e)) return;
Tools.showError(context, e);
});
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,4 @@
package net.kdt.pojavlaunch.mirrors;
public class MirrorTamperedException extends Exception{
}

View File

@@ -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)) {

View File

@@ -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);
}
}
}

View File

@@ -40,5 +40,15 @@
<item>vulkan_zink</item> <!-- virglrenderer with OpenGL ES 3 -->
<item>opengles3_desktopgl_angle_vulkan</item>
</string-array>
<string-array name="download_source_names">
<item>@string/global_default</item>
<item>BMCLAPI</item>
<item>MCBBS</item>
</string-array>
<string-array name="download_source_values">
<item>default</item>
<item>bmclapi</item>
<item>mcbbs</item>
</string-array>
</resources>

View File

@@ -338,4 +338,16 @@
<string name="notif_error_occured">An error has occurred</string>
<string name="notif_error_occured_desc">Click to see more details</string>
<string name="fabric_dl_only_stable">Show only stable versions</string>
<string name="dl_tampered_manifest_title">File verification warning</string>
<string name="dl_tampered_manifest"><![CDATA[The game version manifest on the mirror does not match the official Mojang version manifest, which means it may have been tampered with.<br/>
<b>This is not safe. Your personal information may be at risk if you continue.</b><br/>
If you still want to use the mirror, press \"Turn off manifest checks\" and start the download again.<br/>
If you want to use the official download source, press \"Switch to official site\" and start the download again.<br/>]]>
</string>
<string name="dl_turn_off_manifest_checks">Turn off manifest checks</string>
<string name="dl_switch_to_official_site">Switch to official site</string>
<string name="preference_download_source_title">Game download source</string>
<string name="preference_download_source_description">Select a download mirror instead of using the official download server</string>
<string name="preference_verify_manifest_title">Verify game version manifest</string>
<string name="preference_verify_manifest_description">When enabled, the launcher will check the game version manifest along with the libraries.</string>
</resources>

View File

@@ -16,6 +16,20 @@
android:key="arc_capes"
android:summary="@string/arc_capes_desc"
android:title="@string/arc_capes_title" />
<androidx.preference.ListPreference
android:defaultValue="default"
android:key="downloadSource"
android:entries="@array/download_source_names"
android:entryValues="@array/download_source_values"
android:title="@string/preference_download_source_title"
android:summary="@string/preference_download_source_description"
app2:useSimpleSummaryProvider="true"/>
<SwitchPreference
android:defaultValue="true"
android:key="verifyManifest"
android:title="@string/preference_verify_manifest_title"
android:summary="@string/preference_verify_manifest_description"/>
<SwitchPreference
android:defaultValue="false"
android:key="zinkPreferSystemDriver"