ДОБАВЛЕНИЕ ПРОКСИ РЕЖИМА ЙОООУ 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>
<groupId>me.sashegdev</groupId>
<artifactId>ZernMCLauncher</artifactId>
<version>1.0.4</version>
<version>1.0.5</version>
<build>
<plugins>
<plugin>
+1 -1
View File
@@ -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();
}
@@ -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);
// Используем 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("Не удалось получить версию из " + 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 {
Exception lastException = null;
@@ -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,26 +1,258 @@
package me.sashegdev.zernmc.launcher.utils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.net.URI;
//import java.net.http.HttpClient;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class ZHttpClient {
private static final java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
private static final HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(15))
.version(HttpClient.Version.HTTP_1_1)
.build();
private static final String BASE_URL = "http://87.120.187.36:1582";
public static String get(String endpoint) throws IOException, InterruptedException {
// Глобальный прокси режим (для обратной совместимости)
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(BASE_URL + endpoint))
.timeout(Duration.ofSeconds(15))
.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();
@@ -30,96 +262,243 @@ public class ZHttpClient {
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(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() {
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 get("/launcher/version");
}
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());
public static void forceProxyMode() {
useProxyMode.set(true);
System.out.println(ZAnsi.yellow("Принудительно включен глобальный прокси режим"));
}
// Парсим 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"));
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("Режим прокси выключен"));
}
versions.sort((a, b) -> {
// Правильная семантическая сортировка версий
String[] partsA = a.split("\\+")[0].split("\\.");
String[] partsB = b.split("\\+")[0].split("\\.");
for (int i = 0; i < Math.min(partsA.length, partsB.length); i++) {
int cmp = Integer.compare(Integer.parseInt(partsB[i]), Integer.parseInt(partsA[i]));
if (cmp != 0) return cmp;
}
return b.compareTo(a); // fallback
});
return versions;
public static boolean isProxyMode() {
return useProxyMode.get();
}
public static String downloadString(String url) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
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);
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
}
return response.body();
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);
}
}
}
+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 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__":