diff --git a/launcher/dependency-reduced-pom.xml b/launcher/dependency-reduced-pom.xml
index 93c5bb6..81984aa 100644
--- a/launcher/dependency-reduced-pom.xml
+++ b/launcher/dependency-reduced-pom.xml
@@ -3,7 +3,7 @@
4.0.0
me.sashegdev
ZernMCLauncher
- 1.0.4
+ 1.0.5
diff --git a/launcher/pom.xml b/launcher/pom.xml
index ac776cd..286d7a6 100644
--- a/launcher/pom.xml
+++ b/launcher/pom.xml
@@ -6,7 +6,7 @@
4.0.0
me.sashegdev
ZernMCLauncher
- 1.0.4
+ 1.0.5
jar
diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java
index bb894f7..a5bae5c 100644
--- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java
+++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java
@@ -24,6 +24,9 @@ public class Main {
System.out.print("\033[H\033[2J");
System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION));
+ //проверка всех сервисов при старте
+ ZHttpClient.checkAllServicesOnStartup();
+
checkAndAutoUpdateLauncher();
try {
diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java
index 9af51f5..86be644 100644
--- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java
+++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java
@@ -51,7 +51,6 @@ public class Instance {
saveMetadata();
}
- /** Возвращает ТОТ САМЫЙ assetIndex, который сохранился при установке (например 30) */
public String getAssetIndex() {
return assetIndex != null ? assetIndex : minecraftVersion; // fallback для старых сборок
}
@@ -67,6 +66,7 @@ public class Instance {
public void setFabricVersionId(String fabricVersionId) {
this.fabricVersionId = fabricVersionId;
+ saveMetadata();
}
diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/FabricInstaller.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/FabricInstaller.java
index 8f3abda..ebfb896 100644
--- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/FabricInstaller.java
+++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/FabricInstaller.java
@@ -3,6 +3,7 @@ package me.sashegdev.zernmc.launcher.minecraft.installer;
import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.utils.ProgressBar;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
+import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
import java.io.IOException;
import java.net.URI;
@@ -30,17 +31,14 @@ public class FabricInstaller {
Path instancePath = instance.getPath();
cleanOldFabricLoaders();
- // Шаг 1: Устанавливаем vanilla и получаем правильный assetIndex
VersionInstaller versionInstaller = new VersionInstaller(instancePath);
- String assetIndex = versionInstaller.install(minecraftVersion); // Теперь возвращает "5" вместо "1.20.1"
+ String assetIndex = versionInstaller.install(minecraftVersion);
System.out.println(ZAnsi.green("Asset index получен: " + assetIndex));
- // Сохраняем правильный assetIndex
instance.setAssetIndex(assetIndex);
instance.setMinecraftVersion(minecraftVersion);
- // Шаг 2: Скачивание Fabric Installer
String installerVersion = getLatestInstallerVersion();
String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/"
+ installerVersion + "/fabric-installer-" + installerVersion + ".jar";
@@ -49,17 +47,14 @@ public class FabricInstaller {
if (!Files.exists(installerJar)) {
ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%");
- downloadFile(installerUrl, installerJar);
+ downloadFileWithFallback(installerUrl, installerJar);
ProgressBar.finish("Fabric Installer скачан");
- } else {
- System.out.println(ZAnsi.green("Fabric Installer уже скачан, пропускаем..."));
}
System.out.println(ZAnsi.cyan("Запуск Fabric Installer..."));
-
- // Fabric создаёт версию: fabric-loader-{loaderVersion}-{minecraftVersion}
+
String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion;
-
+
ProcessBuilder pb = new ProcessBuilder(
"java", "-jar", installerJar.toAbsolutePath().toString(),
"client",
@@ -68,7 +63,7 @@ public class FabricInstaller {
"-loader", loaderVersion,
"-noprofile"
);
-
+
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
@@ -80,39 +75,33 @@ public class FabricInstaller {
return false;
}
- // Проверяем, создалась ли папка с Fabric версией
Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId);
if (Files.exists(fabricVersionDir)) {
System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!"));
- System.out.println(ZAnsi.white("Версия: ") + fabricVersionId);
- System.out.println(ZAnsi.white("Asset index: ") + assetIndex);
- // Сохраняем метаданные
instance.setLoaderType("fabric");
instance.setLoaderVersion(loaderVersion);
- instance.setFabricVersionId(fabricVersionId); // <-- ВАЖНО: сохраняем ID Fabric версии
-
- // Исправляем asset index в JSON файле Fabric версии
+ instance.setFabricVersionId(fabricVersionId); // ← СОХРАНЯЕМ
+
ensureAssetIndexInFabricVersion(fabricVersionDir, assetIndex);
return true;
} else {
System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена."));
- System.out.println(ZAnsi.yellow("Искали: " + fabricVersionDir));
-
- // Отладка
- Path versionsDir = instancePath.resolve("versions");
- if (Files.exists(versionsDir)) {
- System.out.println(ZAnsi.cyan("Доступные версии:"));
- try (var stream = Files.list(versionsDir)) {
- stream.forEach(p -> System.out.println(" - " + p.getFileName()));
- }
- }
return false;
}
}
+ private void downloadFileWithFallback(String url, Path target) throws Exception {
+ try {
+ ZHttpClient.downloadFile(url, target);
+ } catch (Exception e) {
+ System.out.println(ZAnsi.yellow("Не удалось скачать Fabric Installer: " + e.getMessage()));
+ throw e;
+ }
+ }
+
private void ensureAssetIndexInFabricVersion(Path fabricVersionDir, String assetIndex) throws IOException {
Path versionJson = fabricVersionDir.resolve(fabricVersionDir.getFileName() + ".json");
@@ -165,25 +154,19 @@ public class FabricInstaller {
}
private String getLatestInstallerVersion() throws Exception {
- String[] urls = {
- "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml",
- "http://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml"
- };
-
- for (String url : urls) {
- try {
- String xml = downloadString(url);
- int start = xml.indexOf("") + 8;
- int end = xml.indexOf("", start);
- return xml.substring(start, end).trim();
- } catch (Exception e) {
- System.out.println(ZAnsi.yellow("Не удалось получить версию из " + url + ": " + e.getMessage()));
- }
+ try {
+ // Используем ZHttpClient с умным прокси
+ String xml = ZHttpClient.downloadString("https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml");
+ int start = xml.indexOf("") + 8;
+ int end = xml.indexOf("", start);
+ return xml.substring(start, end).trim();
+ } catch (Exception e) {
+ System.out.println(ZAnsi.yellow("Ошибка получения версии Fabric Installer: " + e.getMessage()));
+ throw new Exception("Не удалось получить версию Fabric Installer", e);
}
-
- throw new Exception("Не удалось получить версию Fabric Installer");
}
+ // под рефактор оставить
private String downloadString(String url) throws Exception {
Exception lastException = null;
diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java
index c7a5287..9d1f58a 100644
--- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java
+++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java
@@ -39,27 +39,15 @@ public class LaunchCommandBuilder {
String loaderType = instance.getLoaderType().toLowerCase();
if ("forge".equals(loaderType)) {
- // Forge требует особого порядка аргументов
command.addAll(getForgeJvmArguments());
-
- // Forge специфичный classpath
command.add("-cp");
command.add(buildForgeClasspath());
-
- // Главный класс для Forge
command.add("cpw.mods.modlauncher.Launcher");
-
- // Аргументы Forge
command.addAll(getForgeArguments(options));
} else {
- // Стандартный classpath для vanilla/fabric
command.add("-cp");
command.add(buildClasspath());
-
- // Главный класс
command.add(getMainClass());
-
- // Стандартные аргументы Minecraft
command.addAll(getMinecraftArguments(options));
}
@@ -109,7 +97,6 @@ public class LaunchCommandBuilder {
private List getForgeJvmArguments() {
List jvmArgs = new ArrayList<>();
- // Критические аргументы для Forge
jvmArgs.add("--add-modules=ALL-MODULE-PATH");
jvmArgs.add("--add-opens=java.base/java.util.jar=ALL-UNNAMED");
jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED");
@@ -121,7 +108,6 @@ public class LaunchCommandBuilder {
jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED");
jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED");
- // Forge специфичные свойства
jvmArgs.add("-Dforge.logging.console.level=debug");
jvmArgs.add("-Dforge.logging.mojang.level=info");
jvmArgs.add("-DignoreList=bootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,fmlcore,javafmllanguage,lowcodelanguage,mclanguage,forge-");
@@ -133,9 +119,8 @@ public class LaunchCommandBuilder {
private String buildClasspath() throws Exception {
List paths = new ArrayList<>();
- String versionId = getVersionId(); // ← используем getVersionId()
+ String versionId = getVersionId();
- // Добавляем основной jar
Path versionJar = instance.getPath()
.resolve("versions")
.resolve(versionId)
@@ -144,7 +129,6 @@ public class LaunchCommandBuilder {
if (Files.exists(versionJar)) {
paths.add(versionJar.toAbsolutePath().toString());
} else {
- // Fallback на vanilla версию
String mcVersion = instance.getMinecraftVersion();
Path fallbackJar = instance.getPath()
.resolve("versions")
@@ -155,7 +139,6 @@ public class LaunchCommandBuilder {
}
}
- // Все библиотеки
Path librariesDir = instance.getPath().resolve("libraries");
if (Files.exists(librariesDir)) {
try (var stream = Files.walk(librariesDir)) {
@@ -169,16 +152,13 @@ public class LaunchCommandBuilder {
return String.join(separator, paths);
}
-
- // TODO: бля ктонить помогите с этим говном, я рот шатал, форджу не нравится то как я его запускаю
private String buildForgeClasspath() throws Exception {
List paths = new ArrayList<>();
- String versionId = getVersionId(); // ← используем getVersionId()
+ String versionId = getVersionId();
String mcVersion = instance.getMinecraftVersion();
String forgeVersion = instance.getLoaderVersion();
- // 1. Сначала добавляем все библиотеки из libraries
Path librariesDir = instance.getPath().resolve("libraries");
if (Files.exists(librariesDir)) {
try (var stream = Files.walk(librariesDir)) {
@@ -188,7 +168,6 @@ public class LaunchCommandBuilder {
}
}
- // 2. Добавляем jar версии (используя versionId)
Path versionJar = instance.getPath()
.resolve("versions")
.resolve(versionId)
@@ -196,7 +175,6 @@ public class LaunchCommandBuilder {
if (Files.exists(versionJar)) {
paths.add(0, versionJar.toAbsolutePath().toString());
} else {
- // Fallback на vanilla jar
Path vanillaJar = instance.getPath()
.resolve("versions")
.resolve(mcVersion)
@@ -206,7 +184,6 @@ public class LaunchCommandBuilder {
}
}
- // 3. Добавляем Forge universal jar
Path forgeUniversal = instance.getPath()
.resolve("libraries")
.resolve("net")
@@ -218,7 +195,6 @@ public class LaunchCommandBuilder {
paths.add(forgeUniversal.toAbsolutePath().toString());
}
- // 4. Добавляем Forge client jar
Path forgeClient = instance.getPath()
.resolve("libraries")
.resolve("net")
@@ -230,7 +206,6 @@ public class LaunchCommandBuilder {
paths.add(forgeClient.toAbsolutePath().toString());
}
- // 5. Добавляем fmlcore и другие Forge модули
String[] forgeModules = {"fmlcore", "javafmllanguage", "lowcodelanguage", "mclanguage"};
for (String module : forgeModules) {
Path modulePath = instance.getPath()
@@ -263,6 +238,9 @@ public class LaunchCommandBuilder {
}
}
+ /**
+ * ИСПРАВЛЕНО: используем instance.getAssetIndex() вместо minecraftVersion
+ */
private List getMinecraftArguments(LaunchOptions options) {
List args = new ArrayList<>();
@@ -275,8 +253,16 @@ public class LaunchCommandBuilder {
args.add("--assetsDir");
args.add(instance.getPath().resolve("assets").toAbsolutePath().toString());
+ // FIXED: Используем правильный assetIndex
args.add("--assetIndex");
- args.add(instance.getAssetIndex());
+ String assetIndex = instance.getAssetIndex();
+ if (assetIndex == null || assetIndex.isEmpty()) {
+ assetIndex = instance.getMinecraftVersion();
+ System.out.println(ZAnsi.yellow("Asset index не найден, использую версию: " + assetIndex));
+ } else {
+ System.out.println(ZAnsi.green("Использую asset index: " + assetIndex));
+ }
+ args.add(assetIndex);
args.add("--username");
args.add(options.getUsername() != null ? options.getUsername() : "Player");
@@ -302,11 +288,12 @@ public class LaunchCommandBuilder {
return args;
}
- //TODO: сделать это говно удобнее
+ /**
+ * ИСПРАВЛЕНО: для Forge тоже используем правильный assetIndex
+ */
private List getForgeArguments(LaunchOptions options) {
List args = new ArrayList<>();
- // Forge требует специфические аргументы в правильном порядке
args.add("--launchTarget");
args.add("forgeclient");
@@ -319,15 +306,19 @@ public class LaunchCommandBuilder {
args.add("--fml.forgeGroup");
args.add("net.minecraftforge");
- // Добавляем стандартные аргументы Minecraft (Forge их тоже принимает)
args.add("--gameDir");
args.add(instance.getPath().toAbsolutePath().toString());
args.add("--assetsDir");
args.add(instance.getPath().resolve("assets").toAbsolutePath().toString());
+ // FIXED: Используем правильный assetIndex для Forge
args.add("--assetIndex");
- args.add(instance.getAssetIndex());
+ String assetIndex = instance.getAssetIndex();
+ if (assetIndex == null || assetIndex.isEmpty()) {
+ assetIndex = instance.getMinecraftVersion();
+ }
+ args.add(assetIndex);
args.add("--username");
args.add(options.getUsername() != null ? options.getUsername() : "Player");
@@ -353,7 +344,9 @@ public class LaunchCommandBuilder {
return args;
}
- //не трогать, оно работает
+ /**
+ * ИСПРАВЛЕНО: для Fabric используем сохраненный fabricVersionId
+ */
private String getVersionId() {
String loaderType = instance.getLoaderType().toLowerCase();
String mcVersion = instance.getMinecraftVersion();
@@ -363,11 +356,15 @@ public class LaunchCommandBuilder {
return mcVersion;
}
else if ("fabric".equals(loaderType)) {
- // Fabric использует vanilla версию для jar файла
- return mcVersion;
+ // Используем сохраненный fabricVersionId если есть
+ String fabricId = instance.getFabricVersionId();
+ if (fabricId != null && !fabricId.isEmpty()) {
+ return fabricId;
+ }
+ // fallback
+ return "fabric-loader-" + loaderVer + "-" + mcVersion;
}
else if ("forge".equals(loaderType)) {
- // Forge создаёт свою версию в папке versions
return mcVersion + "-forge-" + loaderVer;
}
diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java
index cb46b57..a21e01d 100644
--- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java
+++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java
@@ -1,125 +1,504 @@
package me.sashegdev.zernmc.launcher.utils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
import java.io.IOException;
import java.net.URI;
-//import java.net.http.HttpClient;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
public class ZHttpClient {
- private static final java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder()
- .connectTimeout(Duration.ofSeconds(10))
+ private static final HttpClient client = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(15))
+ .version(HttpClient.Version.HTTP_1_1)
.build();
private static final String BASE_URL = "http://87.120.187.36:1582";
-
- public static String get(String endpoint) throws IOException, InterruptedException {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(BASE_URL + endpoint))
- .timeout(Duration.ofSeconds(15))
- .GET()
- .build();
-
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() != 200) {
- throw new IOException("HTTP " + response.statusCode());
+
+ // Глобальный прокси режим (для обратной совместимости)
+ private static final AtomicBoolean useProxyMode = new AtomicBoolean(false);
+ private static final AtomicBoolean proxyTested = new AtomicBoolean(false);
+
+ // Умное проксирование по сервисам
+ public enum ServiceType {
+ ZERN_SERVER(BASE_URL, true), // Всегда прямое подключение
+ FABRIC_META("https://meta.fabricmc.net", false),
+ FABRIC_MAVEN("https://maven.fabricmc.net", false),
+ MOJANG_META("https://piston-meta.mojang.com", false),
+ MOJANG_RESOURCES("https://resources.download.minecraft.net", false),
+ FORGE_MAVEN("https://maven.minecraftforge.net", false),
+ GOOGLE("https://google.com", false),
+ CLOUDFLARE("https://cloudflare.com", false);
+
+ private final String baseUrl;
+ private final boolean alwaysDirect;
+
+ ServiceType(String baseUrl, boolean alwaysDirect) {
+ this.baseUrl = baseUrl;
+ this.alwaysDirect = alwaysDirect;
}
-
- return response.body();
- }
-
- public static String getBaseUrl() {
- return BASE_URL;
- }
-
- public static String getLauncherVersion() throws IOException, InterruptedException {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(BASE_URL + "/launcher/version"))
- .timeout(Duration.ofSeconds(10))
- .GET()
- .build();
-
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() != 200) {
- throw new IOException("HTTP " + response.statusCode());
+
+ public String getBaseUrl() {
+ return baseUrl;
}
-
- return response.body();
- }
- public static String getLauncherVersionInfo() throws IOException, InterruptedException {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(BASE_URL + "/launcher/version"))
- .timeout(Duration.ofSeconds(10))
- .GET()
- .build();
-
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() != 200) {
- throw new IOException("HTTP " + response.statusCode());
+
+ public boolean isAlwaysDirect() {
+ return alwaysDirect;
}
-
- return response.body();
}
+
+ // Статусы сервисов
+ private static final Map serviceProxyMode = new ConcurrentHashMap<>();
+ private static final Map serviceFailCount = new ConcurrentHashMap<>();
+ private static final Map serviceLastCheckTime = new ConcurrentHashMap<>();
+ private static final Map serviceHealthy = new ConcurrentHashMap<>();
+
+ private static final int MAX_FAILS_BEFORE_PROXY = 2;
+ private static final long HEALTH_CHECK_INTERVAL_MS = 60000; // 1 минута
+ private static final long CHECK_TIMEOUT_MS = 5000; // 5 секунд на проверку
+
+ // Статистика
+ private static int directSuccessCount = 0;
+ private static int proxySuccessCount = 0;
+ private static int directFailCount = 0;
+
+ static {
+ for (ServiceType type : ServiceType.values()) {
+ serviceProxyMode.put(type, false);
+ serviceFailCount.put(type, 0);
+ serviceHealthy.put(type, false);
+ }
+ }
+
/**
- * Получить список всех доступных версий Fabric Loader
+ * Проверить все сервисы при старте
*/
+ public static void checkAllServicesOnStartup() {
+ if (proxyTested.get()) return;
+
+ System.out.println(ZAnsi.cyan("Проверка доступности сервисов..."));
+
+ List servicesToCheck = List.of(
+ ServiceType.ZERN_SERVER,
+ ServiceType.GOOGLE,
+ ServiceType.FABRIC_META,
+ ServiceType.FABRIC_MAVEN,
+ ServiceType.MOJANG_META,
+ ServiceType.FORGE_MAVEN
+ );
+
+ for (ServiceType service : servicesToCheck) {
+ boolean isHealthy = checkServiceHealth(service);
+ serviceHealthy.put(service, isHealthy);
+
+ if (service.isAlwaysDirect()) {
+ if (!isHealthy) {
+ System.out.println(ZAnsi.red(" " + service.name() + " - НЕ ДОСТУПЕН (критично!)"));
+ } else {
+ System.out.println(ZAnsi.green(" " + service.name() + " - OK"));
+ }
+ } else {
+ if (isHealthy) {
+ System.out.println(ZAnsi.green(" " + service.name() + " - прямое подключение работает"));
+ } else {
+ System.out.println(ZAnsi.yellow(" " + service.name() + " - НЕ ДОСТУПЕН, будет использован прокси"));
+ serviceProxyMode.put(service, true);
+ serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
+ }
+ }
+ }
+
+ // Проверяем, нужно ли включить глобальный прокси режим
+ boolean anyCriticalDown = !serviceHealthy.get(ServiceType.ZERN_SERVER);
+ if (anyCriticalDown) {
+ System.out.println(ZAnsi.brightRed("Критическая ошибка: Zern сервер недоступен!"));
+ }
+
+ proxyTested.set(true);
+
+ // Запускаем фоновую проверку
+ startHealthCheckThread();
+
+ printStats();
+ }
+
+ /**
+ * Проверить здоровье конкретного сервиса
+ */
+ private static boolean checkServiceHealth(ServiceType service) {
+ if (service.isAlwaysDirect()) {
+ return checkDirectConnection(service.getBaseUrl());
+ }
+
+ return checkDirectConnection(service.getBaseUrl());
+ }
+
+ /**
+ * Проверить прямое подключение к URL
+ */
+ private static boolean checkDirectConnection(String url) {
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .timeout(Duration.ofMillis(CHECK_TIMEOUT_MS))
+ .HEAD()
+ .build();
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.discarding());
+ return response.statusCode() < 500;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Запустить фоновый поток для периодической проверки
+ */
+ private static void startHealthCheckThread() {
+ Thread healthThread = new Thread(() -> {
+ while (true) {
+ try {
+ Thread.sleep(HEALTH_CHECK_INTERVAL_MS);
+ performHealthCheck();
+ } catch (InterruptedException e) {
+ break;
+ }
+ }
+ });
+ healthThread.setDaemon(true);
+ healthThread.start();
+ }
+
+ /**
+ * Периодическая проверка здоровья сервисов
+ */
+ private static void performHealthCheck() {
+ for (ServiceType service : ServiceType.values()) {
+ if (service.isAlwaysDirect()) continue;
+
+ boolean isHealthy = checkServiceHealth(service);
+ serviceHealthy.put(service, isHealthy);
+
+ if (isHealthy && serviceProxyMode.get(service)) {
+ // Сервис восстановился - пробуем переключить обратно
+ Long lastCheck = serviceLastCheckTime.get(service);
+ if (lastCheck == null || System.currentTimeMillis() - lastCheck > HEALTH_CHECK_INTERVAL_MS) {
+ serviceProxyMode.put(service, false);
+ serviceFailCount.put(service, 0);
+ System.out.println(ZAnsi.green("[NET] " + service.name() + " восстановлен, переключен на прямое подключение"));
+ }
+ } else if (!isHealthy && !serviceProxyMode.get(service)) {
+ int fails = serviceFailCount.getOrDefault(service, 0) + 1;
+ serviceFailCount.put(service, fails);
+ serviceLastCheckTime.put(service, System.currentTimeMillis());
+
+ if (fails >= MAX_FAILS_BEFORE_PROXY) {
+ serviceProxyMode.put(service, true);
+ System.out.println(ZAnsi.yellow("[NET] " + service.name() + " недоступен, включен прокси режим"));
+ }
+ }
+ }
+ }
+
+ /**
+ * Определить тип сервиса по URL
+ */
+ private static ServiceType detectService(String url) {
+ if (url.contains("meta.fabricmc.net")) return ServiceType.FABRIC_META;
+ if (url.contains("maven.fabricmc.net")) return ServiceType.FABRIC_MAVEN;
+ if (url.contains("piston-meta.mojang.com") || url.contains("launchermeta.mojang.com"))
+ return ServiceType.MOJANG_META;
+ if (url.contains("resources.download.minecraft.net")) return ServiceType.MOJANG_RESOURCES;
+ if (url.contains("maven.minecraftforge.net")) return ServiceType.FORGE_MAVEN;
+ if (url.contains("google.com")) return ServiceType.GOOGLE;
+ if (url.contains("cloudflare.com")) return ServiceType.CLOUDFLARE;
+ return null;
+ }
+
+ /**
+ * Нужно ли использовать прокси для URL
+ */
+ private static boolean shouldUseProxyForUrl(String url) {
+ if (useProxyMode.get()) return true;
+
+ ServiceType service = detectService(url);
+ if (service == null) return false;
+ if (service.isAlwaysDirect()) return false;
+
+ return serviceProxyMode.getOrDefault(service, false);
+ }
+
+ /**
+ * Получить URL через прокси если нужно
+ */
+ public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
+ if (shouldUseProxyForUrl(url)) {
+ String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
+ return proxyGet("/download?url=" + encodedUrl);
+ }
+
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .timeout(Duration.ofSeconds(30))
+ .header("User-Agent", "ZernMC-Launcher/1.0")
+ .GET()
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() != 200) {
+ throw new IOException("HTTP " + response.statusCode());
+ }
+
+ directSuccessCount++;
+ return response.body();
+
+ } catch (Exception e) {
+ directFailCount++;
+ ServiceType service = detectService(url);
+ if (service != null && !service.isAlwaysDirect()) {
+ int fails = serviceFailCount.getOrDefault(service, 0) + 1;
+ serviceFailCount.put(service, fails);
+ if (fails >= MAX_FAILS_BEFORE_PROXY) {
+ serviceProxyMode.put(service, true);
+ }
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Скачать файл с умным прокси
+ */
+ public static void downloadFileWithSmartProxy(String url, Path target) throws Exception {
+ if (shouldUseProxyForUrl(url)) {
+ downloadViaProxy(url, target);
+ return;
+ }
+
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .timeout(Duration.ofSeconds(30))
+ .header("User-Agent", "ZernMC-Launcher/1.0")
+ .GET()
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
+
+ if (response.statusCode() != 200) {
+ throw new IOException("HTTP " + response.statusCode());
+ }
+
+ directSuccessCount++;
+
+ } catch (Exception e) {
+ directFailCount++;
+ ServiceType service = detectService(url);
+ if (service != null && !service.isAlwaysDirect()) {
+ int fails = serviceFailCount.getOrDefault(service, 0) + 1;
+ serviceFailCount.put(service, fails);
+ if (fails >= MAX_FAILS_BEFORE_PROXY) {
+ serviceProxyMode.put(service, true);
+ }
+ }
+ throw e;
+ }
+ }
+
+ // ====================== ОСНОВНЫЕ МЕТОДЫ (СОХРАНЕНЫ) ======================
+
+ public static String get(String endpoint) throws IOException, InterruptedException {
+ checkAllServicesOnStartup();
+
+ if (useProxyMode.get()) {
+ return proxyGet(endpoint);
+ }
+
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + endpoint))
+ .timeout(Duration.ofSeconds(15))
+ .header("User-Agent", "ZernMC-Launcher/1.0")
+ .GET()
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() != 200) {
+ throw new IOException("HTTP " + response.statusCode());
+ }
+ return response.body();
+
+ } catch (Exception e) {
+ directFailCount++;
+ throw e;
+ }
+ }
+
+ private static String proxyGet(String endpoint) throws IOException {
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/proxy" + endpoint))
+ .timeout(Duration.ofSeconds(30))
+ .header("User-Agent", "ZernMC-Launcher/1.0")
+ .GET()
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() != 200) {
+ throw new IOException("HTTP " + response.statusCode());
+ }
+
+ proxySuccessCount++;
+ return response.body();
+
+ } catch (Exception e) {
+ throw new IOException("Ошибка прокси запроса: " + e.getMessage(), e);
+ }
+ }
+
+ private static void downloadViaProxy(String url, Path target) throws Exception {
+ String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
+ String proxyUrl = BASE_URL + "/proxy/download?url=" + encodedUrl;
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(proxyUrl))
+ .timeout(Duration.ofMinutes(5))
+ .header("User-Agent", "ZernMC-Launcher/1.0")
+ .GET()
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
+
+ if (response.statusCode() != 200) {
+ throw new IOException("HTTP " + response.statusCode());
+ }
+
+ proxySuccessCount++;
+ }
+
+ // ====================== МЕТОДЫ ДЛЯ EXTERNAL РЕСУРСОВ ======================
+
public static List getFabricLoaderVersions() throws IOException, InterruptedException {
String url = "https://meta.fabricmc.net/v2/versions/loader";
-
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .GET()
- .build();
-
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() != 200) {
- throw new IOException("HTTP " + response.statusCode());
+ String response = getWithSmartProxy(url);
+ return parseFabricVersionsFromJson(response);
+ }
+
+ public static JSONObject getMojangVersionManifest() throws IOException, InterruptedException {
+ String url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
+ String response = getWithSmartProxy(url);
+ return new JSONObject(response);
+ }
+
+ public static JSONObject getMojangVersionJson(String versionId) throws IOException, InterruptedException {
+ JSONObject manifest = getMojangVersionManifest();
+ JSONArray versions = manifest.getJSONArray("versions");
+
+ String versionUrl = null;
+ for (int i = 0; i < versions.length(); i++) {
+ JSONObject v = versions.getJSONObject(i);
+ if (v.getString("id").equals(versionId)) {
+ versionUrl = v.getString("url");
+ break;
+ }
}
-
- // Парсим JSON массив
- org.json.JSONArray array = new org.json.JSONArray(response.body());
+
+ if (versionUrl == null) {
+ throw new IOException("Version " + versionId + " not found");
+ }
+
+ String response = getWithSmartProxy(versionUrl);
+ return new JSONObject(response);
+ }
+
+ public static String getForgeVersionsXml() throws IOException, InterruptedException {
+ String url = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml";
+ return getWithSmartProxy(url);
+ }
+
+ public static void downloadFile(String url, Path target) throws Exception {
+ downloadFileWithSmartProxy(url, target);
+ }
+
+ public static void downloadAsset(String hash, Path target) throws Exception {
+ String url = "https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash;
+ downloadFileWithSmartProxy(url, target);
+ }
+
+ public static String downloadString(String url) throws IOException, InterruptedException {
+ return getWithSmartProxy(url);
+ }
+
+ // ====================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ======================
+
+ private static List parseFabricVersionsFromJson(String json) {
+ JSONArray array = new JSONArray(json);
List versions = new ArrayList<>();
-
for (int i = 0; i < array.length(); i++) {
- org.json.JSONObject obj = array.getJSONObject(i);
+ JSONObject obj = array.getJSONObject(i);
if (obj.has("version")) {
versions.add(obj.getString("version"));
}
}
-
- versions.sort((a, b) -> {
- // Правильная семантическая сортировка версий
- String[] partsA = a.split("\\+")[0].split("\\.");
- String[] partsB = b.split("\\+")[0].split("\\.");
- for (int i = 0; i < Math.min(partsA.length, partsB.length); i++) {
- int cmp = Integer.compare(Integer.parseInt(partsB[i]), Integer.parseInt(partsA[i]));
- if (cmp != 0) return cmp;
- }
- return b.compareTo(a); // fallback
- });
-
return versions;
}
-
- public static String downloadString(String url) throws IOException, InterruptedException {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .GET()
- .build();
-
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() != 200) {
- throw new IOException("HTTP " + response.statusCode());
+
+ public static String getBaseUrl() {
+ return BASE_URL;
+ }
+
+ public static String getLauncherVersionInfo() throws IOException, InterruptedException {
+ return get("/launcher/version");
+ }
+
+ public static void forceProxyMode() {
+ useProxyMode.set(true);
+ System.out.println(ZAnsi.yellow("Принудительно включен глобальный прокси режим"));
+ }
+
+ public static void disableProxyMode() {
+ useProxyMode.set(false);
+ for (ServiceType type : ServiceType.values()) {
+ if (!type.isAlwaysDirect()) {
+ serviceProxyMode.put(type, false);
+ serviceFailCount.put(type, 0);
+ }
+ }
+ System.out.println(ZAnsi.green("Режим прокси выключен"));
+ }
+
+ public static boolean isProxyMode() {
+ return useProxyMode.get();
+ }
+
+ public static void printStats() {
+ System.out.println(ZAnsi.cyan("\n=== Статистика сети ==="));
+ System.out.println(ZAnsi.white("Глобальный прокси режим: ") + (useProxyMode.get() ? "ВКЛЮЧЕН" : "ВЫКЛЮЧЕН"));
+ System.out.println(ZAnsi.white("Прямых успехов: ") + directSuccessCount);
+ System.out.println(ZAnsi.white("Прямых неудач: ") + directFailCount);
+ System.out.println(ZAnsi.white("Прокси успехов: ") + proxySuccessCount);
+
+ System.out.println(ZAnsi.cyan("\nСтатус сервисов:"));
+ for (ServiceType type : ServiceType.values()) {
+ if (type.isAlwaysDirect()) continue;
+ String status = serviceProxyMode.get(type) ? ZAnsi.red("ПРОКСИ") : ZAnsi.green("ПРЯМО");
+ String health = serviceHealthy.get(type) ? ZAnsi.green("✓") : ZAnsi.red("✗");
+ System.out.println(ZAnsi.white(" " + type.name() + ": ") + status + " " + health);
}
- return response.body();
}
}
\ No newline at end of file
diff --git a/server/main.py b/server/main.py
index 5a6107a..9dc25c4 100644
--- a/server/main.py
+++ b/server/main.py
@@ -1,4 +1,4 @@
-from fastapi import FastAPI, HTTPException, Request
+from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import FileResponse, JSONResponse
from contextlib import asynccontextmanager
from pathlib import Path
@@ -16,6 +16,10 @@ from middleware import LoggingMiddleware
from cli import parse_args, run_test_mode, run_production_mode, run_development_mode
from log_manager import init_logging
+import httpx
+import base64
+from fastapi.responses import StreamingResponse
+
logger = structlog.get_logger(__name__)
# Cache for manifests - expires after 5 minutes
@@ -478,6 +482,350 @@ async def get_launcher_full_info():
return info
+# ====================== ПРОКСИ ЭНДПОИНТЫ ======================
+# Эти эндпоинты позволяют клиентам с сетевыми проблемами
+# скачивать файлы через сервер Zern
+
+# Создаем HTTP клиент для прокси
+proxy_client = httpx.AsyncClient(timeout=60.0, follow_redirects=True)
+
+# Кэш для часто запрашиваемых данных (5 минут)
+from cachetools import TTLCache
+proxy_cache = TTLCache(maxsize=50, ttl=300)
+
+# Список заблокированных/проблемных хостов (можно обновлять)
+BLOCKED_HOSTS = []
+
+
+@app.get("/proxy/fabric/versions/loader")
+async def proxy_fabric_versions(request: Request):
+ """Прокси для Fabric Meta API - список версий загрузчика"""
+ client_ip = request.client.host if request.client else "unknown"
+ logger.info(f"Proxy request: Fabric versions from {client_ip}")
+
+ url = "https://meta.fabricmc.net/v2/versions/loader"
+
+ # Проверяем кэш
+ if url in proxy_cache:
+ logger.debug(f"Proxy cache hit for {url}")
+ return JSONResponse(content=proxy_cache[url])
+
+ try:
+ response = await proxy_client.get(url)
+ response.raise_for_status()
+ data = response.json()
+
+ # Кэшируем
+ proxy_cache[url] = data
+
+ logger.info(f"Proxy success: Fabric versions ({len(data)} items)")
+ return JSONResponse(content=data)
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy error for Fabric versions: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+ except Exception as e:
+ logger.error(f"Proxy unexpected error: {e}")
+ raise HTTPException(500, f"Internal error: {str(e)}")
+
+
+@app.get("/proxy/fabric/installer/latest")
+async def proxy_fabric_installer_latest(request: Request):
+ """Получить последнюю версию Fabric Installer"""
+ client_ip = request.client.host if request.client else "unknown"
+ logger.info(f"Proxy request: Fabric installer latest from {client_ip}")
+
+ url = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml"
+
+ try:
+ response = await proxy_client.get(url)
+ response.raise_for_status()
+ xml = response.text
+
+ # Парсим последнюю версию из XML
+ import re
+ match = re.search(r'([^<]+)', xml)
+ if match:
+ version = match.group(1)
+ logger.info(f"Proxy success: Latest Fabric installer version = {version}")
+ return JSONResponse(content={"version": version})
+ else:
+ raise HTTPException(500, "Could not parse latest version")
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy error for Fabric installer latest: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+
+@app.get("/proxy/fabric/installer/{version}")
+async def proxy_fabric_installer_url(version: str, request: Request):
+ """Получить URL для скачивания Fabric Installer определенной версии"""
+ client_ip = request.client.host if request.client else "unknown"
+ logger.info(f"Proxy request: Fabric installer URL for v{version} from {client_ip}")
+
+ url = f"https://maven.fabricmc.net/net/fabricmc/fabric-installer/{version}/fabric-installer-{version}.jar"
+
+ return JSONResponse(content={"url": url, "version": version})
+
+
+@app.get("/proxy/fabric/maven/{path:path}")
+async def proxy_fabric_maven(path: str, request: Request):
+ """Прокси для Fabric Maven файлов"""
+ client_ip = request.client.host if request.client else "unknown"
+ logger.info(f"Proxy request: Fabric Maven {path} from {client_ip}")
+
+ full_url = f"https://maven.fabricmc.net/{path}"
+
+ try:
+ response = await proxy_client.get(full_url)
+ response.raise_for_status()
+
+ # Определяем content-type
+ content_type = "application/octet-stream"
+ if path.endswith(".jar"):
+ content_type = "application/java-archive"
+ elif path.endswith(".pom"):
+ content_type = "application/xml"
+
+ return Response(
+ content=response.content,
+ media_type=content_type,
+ headers={"X-Proxied-By": "ZernMC"}
+ )
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy error for Fabric Maven {path}: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+
+@app.get("/proxy/mojang/version_manifest")
+async def proxy_mojang_manifest(request: Request):
+ """Прокси для Mojang version manifest"""
+ client_ip = request.client.host if request.client else "unknown"
+ logger.info(f"Proxy request: Mojang manifest from {client_ip}")
+
+ url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
+
+ if url in proxy_cache:
+ return JSONResponse(content=proxy_cache[url])
+
+ try:
+ response = await proxy_client.get(url)
+ response.raise_for_status()
+ data = response.json()
+
+ proxy_cache[url] = data
+ logger.info("Proxy success: Mojang manifest")
+ return JSONResponse(content=data)
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy error for Mojang manifest: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+
+@app.get("/proxy/mojang/version/{version_id}")
+async def proxy_mojang_version(version_id: str, request: Request):
+ """Прокси для конкретной версии Mojang"""
+ client_ip = request.client.host if request.client else "unknown"
+ logger.info(f"Proxy request: Mojang version {version_id} from {client_ip}")
+
+ # Сначала получаем манифест, чтобы найти URL версии
+ manifest_url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
+
+ cache_key = f"version_url_{version_id}"
+ version_url = proxy_cache.get(cache_key)
+
+ if not version_url:
+ try:
+ response = await proxy_client.get(manifest_url)
+ response.raise_for_status()
+ manifest = response.json()
+
+ for version in manifest.get("versions", []):
+ if version.get("id") == version_id:
+ version_url = version.get("url")
+ proxy_cache[cache_key] = version_url
+ break
+
+ if not version_url:
+ raise HTTPException(404, f"Version {version_id} not found")
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy error getting version URL: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+ try:
+ response = await proxy_client.get(version_url)
+ response.raise_for_status()
+ data = response.json()
+
+ logger.info(f"Proxy success: Mojang version {version_id}")
+ return JSONResponse(content=data)
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy error for Mojang version {version_id}: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+
+@app.get("/proxy/forge/versions")
+async def proxy_forge_versions(request: Request):
+ """Прокси для списка версий Forge"""
+ client_ip = request.client.host if request.client else "unknown"
+ logger.info(f"Proxy request: Forge versions from {client_ip}")
+
+ url = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml"
+
+ try:
+ response = await proxy_client.get(url)
+ response.raise_for_status()
+
+ # Возвращаем XML как есть
+ return Response(
+ content=response.content,
+ media_type="application/xml",
+ headers={"X-Proxied-By": "ZernMC"}
+ )
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy error for Forge versions: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+
+@app.get("/proxy/forge/maven/{path:path}")
+async def proxy_forge_maven(path: str, request: Request):
+ """Прокси для Forge Maven файлов"""
+ client_ip = request.client.host if request.client else "unknown"
+ logger.info(f"Proxy request: Forge Maven {path} from {client_ip}")
+
+ full_url = f"https://maven.minecraftforge.net/{path}"
+
+ try:
+ response = await proxy_client.get(full_url)
+ response.raise_for_status()
+
+ content_type = "application/octet-stream"
+ if path.endswith(".jar"):
+ content_type = "application/java-archive"
+ elif path.endswith(".pom"):
+ content_type = "application/xml"
+
+ return Response(
+ content=response.content,
+ media_type=content_type,
+ headers={"X-Proxied-By": "ZernMC"}
+ )
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy error for Forge Maven {path}: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+
+@app.get("/proxy/download")
+async def proxy_download(request: Request):
+ """Универсальный прокси для скачивания файлов"""
+ client_ip = request.client.host if request.client else "unknown"
+ url = request.query_params.get("url")
+
+ if not url:
+ raise HTTPException(400, "Missing 'url' parameter")
+
+ # Безопасность: проверяем URL
+ allowed_domains = [
+ "maven.fabricmc.net",
+ "meta.fabricmc.net",
+ "piston-meta.mojang.com",
+ "launchermeta.mojang.com",
+ "resources.download.minecraft.net",
+ "maven.minecraftforge.net",
+ "files.minecraftforge.net"
+ ]
+
+ # Проверяем, что URL ведет на разрешенный домен
+ from urllib.parse import urlparse
+ parsed = urlparse(url)
+ domain = parsed.netloc.lower()
+
+ # Убираем порт если есть
+ domain = domain.split(':')[0]
+
+ if domain not in allowed_domains and not any(domain.endswith(f".{d}") for d in allowed_domains):
+ logger.warning(f"Proxy blocked: {domain} not in allowed list (client: {client_ip}, url: {url[:100]})")
+ raise HTTPException(403, f"Domain {domain} not allowed")
+
+ logger.info(f"Proxy download: {url[:100]}... from {client_ip}")
+
+ try:
+ # Используем streaming response для больших файлов
+ response = await proxy_client.get(url)
+ response.raise_for_status()
+
+ # Определяем content-type из ответа или по расширению
+ content_type = response.headers.get("content-type", "application/octet-stream")
+
+ return Response(
+ content=response.content,
+ media_type=content_type,
+ headers={
+ "X-Proxied-By": "ZernMC",
+ "X-Original-Url": url[:100] # только для отладки
+ }
+ )
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy download error for {url[:100]}: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+
+@app.get("/proxy/asset/{hash}")
+async def proxy_asset(hash: str, request: Request):
+ """Прокси для Minecraft ассетов по хешу"""
+ client_ip = request.client.host if request.client else "unknown"
+
+ if len(hash) < 2:
+ raise HTTPException(400, "Invalid hash")
+
+ url = f"https://resources.download.minecraft.net/{hash[:2]}/{hash}"
+ logger.info(f"Proxy asset: {hash} from {client_ip}")
+
+ try:
+ response = await proxy_client.get(url)
+ response.raise_for_status()
+
+ return Response(
+ content=response.content,
+ media_type="application/octet-stream",
+ headers={"X-Proxied-By": "ZernMC"}
+ )
+
+ except httpx.HTTPError as e:
+ logger.error(f"Proxy asset error for {hash}: {e}")
+ raise HTTPException(502, f"Bad Gateway: {str(e)}")
+
+
+@app.get("/proxy/status")
+async def proxy_status():
+ """Проверка статуса прокси сервера"""
+ return {
+ "status": "ok",
+ "cached_items": len(proxy_cache),
+ "allowed_domains": [
+ "maven.fabricmc.net",
+ "meta.fabricmc.net",
+ "piston-meta.mojang.com",
+ "launchermeta.mojang.com",
+ "resources.download.minecraft.net",
+ "maven.minecraftforge.net"
+ ],
+ "note": "Use this proxy if you have network issues connecting to Fabric/Mojang/Forge"
+ }
+
+
+# Cleanup on shutdown
+@app.on_event("shutdown")
+async def shutdown_proxy():
+ await proxy_client.close()
+
+
# ====================== ЗАПУСК ======================
if __name__ == "__main__":