ДОБАВЛЕНИЕ ПРОКСИ РЕЖИМА ЙОООУ 1.0.5

This commit is contained in:
Sashegdev
2026-04-06 19:57:32 +00:00
parent b47793b618
commit c03d7a788f
8 changed files with 884 additions and 174 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>me.sashegdev</groupId> <groupId>me.sashegdev</groupId>
<artifactId>ZernMCLauncher</artifactId> <artifactId>ZernMCLauncher</artifactId>
<version>1.0.4</version> <version>1.0.5</version>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
+1 -1
View File
@@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>me.sashegdev</groupId> <groupId>me.sashegdev</groupId>
<artifactId>ZernMCLauncher</artifactId> <artifactId>ZernMCLauncher</artifactId>
<version>1.0.4</version> <version>1.0.5</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
@@ -24,6 +24,9 @@ public class Main {
System.out.print("\033[H\033[2J"); System.out.print("\033[H\033[2J");
System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION)); System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION));
//проверка всех сервисов при старте
ZHttpClient.checkAllServicesOnStartup();
checkAndAutoUpdateLauncher(); checkAndAutoUpdateLauncher();
try { try {
@@ -51,7 +51,6 @@ public class Instance {
saveMetadata(); saveMetadata();
} }
/** Возвращает ТОТ САМЫЙ assetIndex, который сохранился при установке (например 30) */
public String getAssetIndex() { public String getAssetIndex() {
return assetIndex != null ? assetIndex : minecraftVersion; // fallback для старых сборок return assetIndex != null ? assetIndex : minecraftVersion; // fallback для старых сборок
} }
@@ -67,6 +66,7 @@ public class Instance {
public void setFabricVersionId(String fabricVersionId) { public void setFabricVersionId(String fabricVersionId) {
this.fabricVersionId = fabricVersionId; this.fabricVersionId = fabricVersionId;
saveMetadata();
} }
@@ -3,6 +3,7 @@ package me.sashegdev.zernmc.launcher.minecraft.installer;
import me.sashegdev.zernmc.launcher.minecraft.Instance; import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.utils.ProgressBar; import me.sashegdev.zernmc.launcher.utils.ProgressBar;
import me.sashegdev.zernmc.launcher.utils.ZAnsi; import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@@ -30,17 +31,14 @@ public class FabricInstaller {
Path instancePath = instance.getPath(); Path instancePath = instance.getPath();
cleanOldFabricLoaders(); cleanOldFabricLoaders();
// Шаг 1: Устанавливаем vanilla и получаем правильный assetIndex
VersionInstaller versionInstaller = new VersionInstaller(instancePath); 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)); System.out.println(ZAnsi.green("Asset index получен: " + assetIndex));
// Сохраняем правильный assetIndex
instance.setAssetIndex(assetIndex); instance.setAssetIndex(assetIndex);
instance.setMinecraftVersion(minecraftVersion); instance.setMinecraftVersion(minecraftVersion);
// Шаг 2: Скачивание Fabric Installer
String installerVersion = getLatestInstallerVersion(); String installerVersion = getLatestInstallerVersion();
String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/" String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/"
+ installerVersion + "/fabric-installer-" + installerVersion + ".jar"; + installerVersion + "/fabric-installer-" + installerVersion + ".jar";
@@ -49,15 +47,12 @@ public class FabricInstaller {
if (!Files.exists(installerJar)) { if (!Files.exists(installerJar)) {
ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%"); ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%");
downloadFile(installerUrl, installerJar); downloadFileWithFallback(installerUrl, installerJar);
ProgressBar.finish("Fabric Installer скачан"); ProgressBar.finish("Fabric Installer скачан");
} else {
System.out.println(ZAnsi.green("Fabric Installer уже скачан, пропускаем..."));
} }
System.out.println(ZAnsi.cyan("Запуск Fabric Installer...")); System.out.println(ZAnsi.cyan("Запуск Fabric Installer..."));
// Fabric создаёт версию: fabric-loader-{loaderVersion}-{minecraftVersion}
String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion; String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion;
ProcessBuilder pb = new ProcessBuilder( ProcessBuilder pb = new ProcessBuilder(
@@ -80,39 +75,33 @@ public class FabricInstaller {
return false; return false;
} }
// Проверяем, создалась ли папка с Fabric версией
Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId); Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId);
if (Files.exists(fabricVersionDir)) { if (Files.exists(fabricVersionDir)) {
System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!")); System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!"));
System.out.println(ZAnsi.white("Версия: ") + fabricVersionId);
System.out.println(ZAnsi.white("Asset index: ") + assetIndex);
// Сохраняем метаданные
instance.setLoaderType("fabric"); instance.setLoaderType("fabric");
instance.setLoaderVersion(loaderVersion); instance.setLoaderVersion(loaderVersion);
instance.setFabricVersionId(fabricVersionId); // <-- ВАЖНО: сохраняем ID Fabric версии instance.setFabricVersionId(fabricVersionId); // ← СОХРАНЯЕМ
// Исправляем asset index в JSON файле Fabric версии
ensureAssetIndexInFabricVersion(fabricVersionDir, assetIndex); ensureAssetIndexInFabricVersion(fabricVersionDir, assetIndex);
return true; return true;
} else { } else {
System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена.")); 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; 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 { private void ensureAssetIndexInFabricVersion(Path fabricVersionDir, String assetIndex) throws IOException {
Path versionJson = fabricVersionDir.resolve(fabricVersionDir.getFileName() + ".json"); Path versionJson = fabricVersionDir.resolve(fabricVersionDir.getFileName() + ".json");
@@ -165,25 +154,19 @@ public class FabricInstaller {
} }
private String getLatestInstallerVersion() throws Exception { 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 { try {
String xml = downloadString(url); // Используем ZHttpClient с умным прокси
String xml = ZHttpClient.downloadString("https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml");
int start = xml.indexOf("<latest>") + 8; int start = xml.indexOf("<latest>") + 8;
int end = xml.indexOf("</latest>", start); int end = xml.indexOf("</latest>", start);
return xml.substring(start, end).trim(); return xml.substring(start, end).trim();
} catch (Exception e) { } catch (Exception e) {
System.out.println(ZAnsi.yellow("Не удалось получить версию из " + url + ": " + e.getMessage())); 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 { private String downloadString(String url) throws Exception {
Exception lastException = null; Exception lastException = null;
@@ -39,27 +39,15 @@ public class LaunchCommandBuilder {
String loaderType = instance.getLoaderType().toLowerCase(); String loaderType = instance.getLoaderType().toLowerCase();
if ("forge".equals(loaderType)) { if ("forge".equals(loaderType)) {
// Forge требует особого порядка аргументов
command.addAll(getForgeJvmArguments()); command.addAll(getForgeJvmArguments());
// Forge специфичный classpath
command.add("-cp"); command.add("-cp");
command.add(buildForgeClasspath()); command.add(buildForgeClasspath());
// Главный класс для Forge
command.add("cpw.mods.modlauncher.Launcher"); command.add("cpw.mods.modlauncher.Launcher");
// Аргументы Forge
command.addAll(getForgeArguments(options)); command.addAll(getForgeArguments(options));
} else { } else {
// Стандартный classpath для vanilla/fabric
command.add("-cp"); command.add("-cp");
command.add(buildClasspath()); command.add(buildClasspath());
// Главный класс
command.add(getMainClass()); command.add(getMainClass());
// Стандартные аргументы Minecraft
command.addAll(getMinecraftArguments(options)); command.addAll(getMinecraftArguments(options));
} }
@@ -109,7 +97,6 @@ public class LaunchCommandBuilder {
private List<String> getForgeJvmArguments() { private List<String> getForgeJvmArguments() {
List<String> jvmArgs = new ArrayList<>(); List<String> jvmArgs = new ArrayList<>();
// Критические аргументы для Forge
jvmArgs.add("--add-modules=ALL-MODULE-PATH"); 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.util.jar=ALL-UNNAMED");
jvmArgs.add("--add-opens=java.base/java.lang.invoke=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=java.base/sun.nio.ch=ALL-UNNAMED");
jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED"); jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED");
// Forge специфичные свойства
jvmArgs.add("-Dforge.logging.console.level=debug"); jvmArgs.add("-Dforge.logging.console.level=debug");
jvmArgs.add("-Dforge.logging.mojang.level=info"); 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-"); 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 { private String buildClasspath() throws Exception {
List<String> paths = new ArrayList<>(); List<String> paths = new ArrayList<>();
String versionId = getVersionId(); // ← используем getVersionId() String versionId = getVersionId();
// Добавляем основной jar
Path versionJar = instance.getPath() Path versionJar = instance.getPath()
.resolve("versions") .resolve("versions")
.resolve(versionId) .resolve(versionId)
@@ -144,7 +129,6 @@ public class LaunchCommandBuilder {
if (Files.exists(versionJar)) { if (Files.exists(versionJar)) {
paths.add(versionJar.toAbsolutePath().toString()); paths.add(versionJar.toAbsolutePath().toString());
} else { } else {
// Fallback на vanilla версию
String mcVersion = instance.getMinecraftVersion(); String mcVersion = instance.getMinecraftVersion();
Path fallbackJar = instance.getPath() Path fallbackJar = instance.getPath()
.resolve("versions") .resolve("versions")
@@ -155,7 +139,6 @@ public class LaunchCommandBuilder {
} }
} }
// Все библиотеки
Path librariesDir = instance.getPath().resolve("libraries"); Path librariesDir = instance.getPath().resolve("libraries");
if (Files.exists(librariesDir)) { if (Files.exists(librariesDir)) {
try (var stream = Files.walk(librariesDir)) { try (var stream = Files.walk(librariesDir)) {
@@ -169,16 +152,13 @@ public class LaunchCommandBuilder {
return String.join(separator, paths); return String.join(separator, paths);
} }
// TODO: бля ктонить помогите с этим говном, я рот шатал, форджу не нравится то как я его запускаю
private String buildForgeClasspath() throws Exception { private String buildForgeClasspath() throws Exception {
List<String> paths = new ArrayList<>(); List<String> paths = new ArrayList<>();
String versionId = getVersionId(); // ← используем getVersionId() String versionId = getVersionId();
String mcVersion = instance.getMinecraftVersion(); String mcVersion = instance.getMinecraftVersion();
String forgeVersion = instance.getLoaderVersion(); String forgeVersion = instance.getLoaderVersion();
// 1. Сначала добавляем все библиотеки из libraries
Path librariesDir = instance.getPath().resolve("libraries"); Path librariesDir = instance.getPath().resolve("libraries");
if (Files.exists(librariesDir)) { if (Files.exists(librariesDir)) {
try (var stream = Files.walk(librariesDir)) { try (var stream = Files.walk(librariesDir)) {
@@ -188,7 +168,6 @@ public class LaunchCommandBuilder {
} }
} }
// 2. Добавляем jar версии (используя versionId)
Path versionJar = instance.getPath() Path versionJar = instance.getPath()
.resolve("versions") .resolve("versions")
.resolve(versionId) .resolve(versionId)
@@ -196,7 +175,6 @@ public class LaunchCommandBuilder {
if (Files.exists(versionJar)) { if (Files.exists(versionJar)) {
paths.add(0, versionJar.toAbsolutePath().toString()); paths.add(0, versionJar.toAbsolutePath().toString());
} else { } else {
// Fallback на vanilla jar
Path vanillaJar = instance.getPath() Path vanillaJar = instance.getPath()
.resolve("versions") .resolve("versions")
.resolve(mcVersion) .resolve(mcVersion)
@@ -206,7 +184,6 @@ public class LaunchCommandBuilder {
} }
} }
// 3. Добавляем Forge universal jar
Path forgeUniversal = instance.getPath() Path forgeUniversal = instance.getPath()
.resolve("libraries") .resolve("libraries")
.resolve("net") .resolve("net")
@@ -218,7 +195,6 @@ public class LaunchCommandBuilder {
paths.add(forgeUniversal.toAbsolutePath().toString()); paths.add(forgeUniversal.toAbsolutePath().toString());
} }
// 4. Добавляем Forge client jar
Path forgeClient = instance.getPath() Path forgeClient = instance.getPath()
.resolve("libraries") .resolve("libraries")
.resolve("net") .resolve("net")
@@ -230,7 +206,6 @@ public class LaunchCommandBuilder {
paths.add(forgeClient.toAbsolutePath().toString()); paths.add(forgeClient.toAbsolutePath().toString());
} }
// 5. Добавляем fmlcore и другие Forge модули
String[] forgeModules = {"fmlcore", "javafmllanguage", "lowcodelanguage", "mclanguage"}; String[] forgeModules = {"fmlcore", "javafmllanguage", "lowcodelanguage", "mclanguage"};
for (String module : forgeModules) { for (String module : forgeModules) {
Path modulePath = instance.getPath() Path modulePath = instance.getPath()
@@ -263,6 +238,9 @@ public class LaunchCommandBuilder {
} }
} }
/**
* ИСПРАВЛЕНО: используем instance.getAssetIndex() вместо minecraftVersion
*/
private List<String> getMinecraftArguments(LaunchOptions options) { private List<String> getMinecraftArguments(LaunchOptions options) {
List<String> args = new ArrayList<>(); List<String> args = new ArrayList<>();
@@ -275,8 +253,16 @@ public class LaunchCommandBuilder {
args.add("--assetsDir"); args.add("--assetsDir");
args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); args.add(instance.getPath().resolve("assets").toAbsolutePath().toString());
// FIXED: Используем правильный assetIndex
args.add("--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("--username");
args.add(options.getUsername() != null ? options.getUsername() : "Player"); args.add(options.getUsername() != null ? options.getUsername() : "Player");
@@ -302,11 +288,12 @@ public class LaunchCommandBuilder {
return args; return args;
} }
//TODO: сделать это говно удобнее /**
* ИСПРАВЛЕНО: для Forge тоже используем правильный assetIndex
*/
private List<String> getForgeArguments(LaunchOptions options) { private List<String> getForgeArguments(LaunchOptions options) {
List<String> args = new ArrayList<>(); List<String> args = new ArrayList<>();
// Forge требует специфические аргументы в правильном порядке
args.add("--launchTarget"); args.add("--launchTarget");
args.add("forgeclient"); args.add("forgeclient");
@@ -319,15 +306,19 @@ public class LaunchCommandBuilder {
args.add("--fml.forgeGroup"); args.add("--fml.forgeGroup");
args.add("net.minecraftforge"); args.add("net.minecraftforge");
// Добавляем стандартные аргументы Minecraft (Forge их тоже принимает)
args.add("--gameDir"); args.add("--gameDir");
args.add(instance.getPath().toAbsolutePath().toString()); args.add(instance.getPath().toAbsolutePath().toString());
args.add("--assetsDir"); args.add("--assetsDir");
args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); args.add(instance.getPath().resolve("assets").toAbsolutePath().toString());
// FIXED: Используем правильный assetIndex для Forge
args.add("--assetIndex"); 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("--username");
args.add(options.getUsername() != null ? options.getUsername() : "Player"); args.add(options.getUsername() != null ? options.getUsername() : "Player");
@@ -353,7 +344,9 @@ public class LaunchCommandBuilder {
return args; return args;
} }
//не трогать, оно работает /**
* ИСПРАВЛЕНО: для Fabric используем сохраненный fabricVersionId
*/
private String getVersionId() { private String getVersionId() {
String loaderType = instance.getLoaderType().toLowerCase(); String loaderType = instance.getLoaderType().toLowerCase();
String mcVersion = instance.getMinecraftVersion(); String mcVersion = instance.getMinecraftVersion();
@@ -363,11 +356,15 @@ public class LaunchCommandBuilder {
return mcVersion; return mcVersion;
} }
else if ("fabric".equals(loaderType)) { else if ("fabric".equals(loaderType)) {
// Fabric использует vanilla версию для jar файла // Используем сохраненный fabricVersionId если есть
return mcVersion; String fabricId = instance.getFabricVersionId();
if (fabricId != null && !fabricId.isEmpty()) {
return fabricId;
}
// fallback
return "fabric-loader-" + loaderVer + "-" + mcVersion;
} }
else if ("forge".equals(loaderType)) { else if ("forge".equals(loaderType)) {
// Forge создаёт свою версию в папке versions
return mcVersion + "-forge-" + loaderVer; return mcVersion + "-forge-" + loaderVer;
} }
@@ -1,26 +1,258 @@
package me.sashegdev.zernmc.launcher.utils; package me.sashegdev.zernmc.launcher.utils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.net.URI; 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.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class ZHttpClient { public class ZHttpClient {
private static final java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder() private static final HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) .connectTimeout(Duration.ofSeconds(15))
.version(HttpClient.Version.HTTP_1_1)
.build(); .build();
private static final String BASE_URL = "http://87.120.187.36:1582"; private static final String BASE_URL = "http://87.120.187.36:1582";
public static String get(String endpoint) throws IOException, InterruptedException { // Глобальный прокси режим (для обратной совместимости)
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;
}
public String getBaseUrl() {
return baseUrl;
}
public boolean isAlwaysDirect() {
return alwaysDirect;
}
}
// Статусы сервисов
private static final Map<ServiceType, Boolean> serviceProxyMode = new ConcurrentHashMap<>();
private static final Map<ServiceType, Integer> serviceFailCount = new ConcurrentHashMap<>();
private static final Map<ServiceType, Long> serviceLastCheckTime = new ConcurrentHashMap<>();
private static final Map<ServiceType, Boolean> 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);
}
}
/**
* Проверить все сервисы при старте
*/
public static void checkAllServicesOnStartup() {
if (proxyTested.get()) return;
System.out.println(ZAnsi.cyan("Проверка доступности сервисов..."));
List<ServiceType> 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() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + endpoint)) .uri(URI.create(url))
.timeout(Duration.ofSeconds(15)) .timeout(Duration.ofMillis(CHECK_TIMEOUT_MS))
.HEAD()
.build();
HttpResponse<Void> 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() .GET()
.build(); .build();
@@ -30,96 +262,243 @@ public class ZHttpClient {
throw new IOException("HTTP " + response.statusCode()); throw new IOException("HTTP " + response.statusCode());
} }
directSuccessCount++;
return response.body(); 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<Path> 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<String> 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<String> 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<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
}
proxySuccessCount++;
}
// ====================== МЕТОДЫ ДЛЯ EXTERNAL РЕСУРСОВ ======================
public static List<String> getFabricLoaderVersions() throws IOException, InterruptedException {
String url = "https://meta.fabricmc.net/v2/versions/loader";
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;
}
}
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<String> parseFabricVersionsFromJson(String json) {
JSONArray array = new JSONArray(json);
List<String> versions = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
if (obj.has("version")) {
versions.add(obj.getString("version"));
}
}
return versions;
} }
public static String getBaseUrl() { public static String getBaseUrl() {
return BASE_URL; 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<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
}
return response.body();
}
public static String getLauncherVersionInfo() throws IOException, InterruptedException { public static String getLauncherVersionInfo() throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder() return get("/launcher/version");
.uri(URI.create(BASE_URL + "/launcher/version"))
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
} }
return response.body(); public static void forceProxyMode() {
} useProxyMode.set(true);
/** System.out.println(ZAnsi.yellow("Принудительно включен глобальный прокси режим"));
* Получить список всех доступных версий Fabric Loader
*/
public static List<String> getFabricLoaderVersions() throws IOException, InterruptedException {
String url = "https://meta.fabricmc.net/v2/versions/loader";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
} }
// Парсим JSON массив public static void disableProxyMode() {
org.json.JSONArray array = new org.json.JSONArray(response.body()); useProxyMode.set(false);
List<String> versions = new ArrayList<>(); for (ServiceType type : ServiceType.values()) {
if (!type.isAlwaysDirect()) {
for (int i = 0; i < array.length(); i++) { serviceProxyMode.put(type, false);
org.json.JSONObject obj = array.getJSONObject(i); serviceFailCount.put(type, 0);
if (obj.has("version")) {
versions.add(obj.getString("version"));
} }
} }
System.out.println(ZAnsi.green("Режим прокси выключен"));
}
versions.sort((a, b) -> { public static boolean isProxyMode() {
// Правильная семантическая сортировка версий return useProxyMode.get();
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 { public static void printStats() {
HttpRequest request = HttpRequest.newBuilder() System.out.println(ZAnsi.cyan("\n=== Статистика сети ==="));
.uri(URI.create(url)) System.out.println(ZAnsi.white("Глобальный прокси режим: ") + (useProxyMode.get() ? "ВКЛЮЧЕН" : "ВЫКЛЮЧЕН"));
.GET() System.out.println(ZAnsi.white("Прямых успехов: ") + directSuccessCount);
.build(); System.out.println(ZAnsi.white("Прямых неудач: ") + directFailCount);
System.out.println(ZAnsi.white("Прокси успехов: ") + proxySuccessCount);
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(ZAnsi.cyan("\nСтатус сервисов:"));
for (ServiceType type : ServiceType.values()) {
if (response.statusCode() != 200) { if (type.isAlwaysDirect()) continue;
throw new IOException("HTTP " + response.statusCode()); String status = serviceProxyMode.get(type) ? ZAnsi.red("ПРОКСИ") : ZAnsi.green("ПРЯМО");
} String health = serviceHealthy.get(type) ? ZAnsi.green("") : ZAnsi.red("");
return response.body(); System.out.println(ZAnsi.white(" " + type.name() + ": ") + status + " " + health);
}
} }
} }
+349 -1
View File
@@ -1,4 +1,4 @@
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import FileResponse, JSONResponse from fastapi.responses import FileResponse, JSONResponse
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path 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 cli import parse_args, run_test_mode, run_production_mode, run_development_mode
from log_manager import init_logging from log_manager import init_logging
import httpx
import base64
from fastapi.responses import StreamingResponse
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
# Cache for manifests - expires after 5 minutes # Cache for manifests - expires after 5 minutes
@@ -478,6 +482,350 @@ async def get_launcher_full_info():
return 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'<latest>([^<]+)</latest>', 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__": if __name__ == "__main__":