ДОБАВЛЕНИЕ ПРОКСИ РЕЖИМА ЙОООУ 1.0.5
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>me.sashegdev</groupId>
|
||||
<artifactId>ZernMCLauncher</artifactId>
|
||||
<version>1.0.4</version>
|
||||
<version>1.0.5</version>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>me.sashegdev</groupId>
|
||||
<artifactId>ZernMCLauncher</artifactId>
|
||||
<version>1.0.4</version>
|
||||
<version>1.0.5</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
+23
-40
@@ -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,15 +47,12 @@ 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(
|
||||
@@ -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 версии
|
||||
instance.setFabricVersionId(fabricVersionId); // ← СОХРАНЯЕМ
|
||||
|
||||
// Исправляем asset index в JSON файле Fabric версии
|
||||
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("<latest>") + 8;
|
||||
int end = xml.indexOf("</latest>", 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("<latest>") + 8;
|
||||
int end = xml.indexOf("</latest>", 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;
|
||||
|
||||
|
||||
+33
-36
@@ -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<String> getForgeJvmArguments() {
|
||||
List<String> 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<String> 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<String> 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<String> getMinecraftArguments(LaunchOptions options) {
|
||||
List<String> 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<String> getForgeArguments(LaunchOptions options) {
|
||||
List<String> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
// Глобальный прокси режим (для обратной совместимости)
|
||||
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()
|
||||
.uri(URI.create(url))
|
||||
.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()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> 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<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(BASE_URL + endpoint))
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
.uri(URI.create(proxyUrl))
|
||||
.timeout(Duration.ofMinutes(5))
|
||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("HTTP " + response.statusCode());
|
||||
}
|
||||
|
||||
return response.body();
|
||||
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() {
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
/**
|
||||
* Получить список всех доступных версий 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 массив
|
||||
org.json.JSONArray array = new org.json.JSONArray(response.body());
|
||||
List<String> versions = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
org.json.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;
|
||||
return get("/launcher/version");
|
||||
}
|
||||
|
||||
public static String downloadString(String url) throws IOException, InterruptedException {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.GET()
|
||||
.build();
|
||||
public static void forceProxyMode() {
|
||||
useProxyMode.set(true);
|
||||
System.out.println(ZAnsi.yellow("Принудительно включен глобальный прокси режим"));
|
||||
}
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("HTTP " + response.statusCode());
|
||||
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();
|
||||
}
|
||||
}
|
||||
+349
-1
@@ -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'<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__":
|
||||
|
||||
Reference in New Issue
Block a user