Попытка добавления проходок, аккаунтов, а так же доработка прокси

This commit is contained in:
Sashegdev
2026-04-07 17:50:29 +00:00
parent c03d7a788f
commit 9bee361ea4
11 changed files with 1198 additions and 219 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>me.sashegdev</groupId>
<artifactId>ZernMCLauncher</artifactId>
<version>1.0.5</version>
<version>1.0.6</version>
<build>
<plugins>
<plugin>
@@ -1,5 +1,6 @@
package me.sashegdev.zernmc.launcher;
import me.sashegdev.zernmc.launcher.auth.AuthManager;
import me.sashegdev.zernmc.launcher.menu.*;
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
import me.sashegdev.zernmc.launcher.utils.*;
@@ -17,8 +18,12 @@ public class Main {
private static final String CURRENT_VERSION = Version.getCurrentVersion();
public static void main(String[] args) {
public static void main(String[] args) throws IOException {
System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true");
System.setProperty("file.encoding", "UTF-8");
System.setProperty("sun.err.encoding", "UTF-8");
System.setProperty("sun.stdout.encoding", "UTF-8");
java.nio.charset.Charset.defaultCharset();
ZAnsi.install();
System.out.print("\033[H\033[2J");
@@ -29,6 +34,23 @@ public class Main {
checkAndAutoUpdateLauncher();
// === АВТОРИЗАЦИЯ ===
System.out.println(ZAnsi.cyan("Проверка авторизации..."));
boolean sessionRestored = AuthManager.loadSavedSession();
if (!sessionRestored) {
LoginMenu loginMenu = new LoginMenu();
boolean loggedIn = loginMenu.show();
if (!loggedIn) {
System.out.println(ZAnsi.yellow("До свидания!"));
ZAnsi.uninstall();
System.exit(0);
}
} else {
System.out.println(ZAnsi.brightGreen("Добро пожаловать обратно, " + AuthManager.getUsername() + "!"));
}
// === КОНЕЦ АВТОРИЗАЦИИ ===
try {
mainLoop();
} catch (Exception e) {
@@ -0,0 +1,206 @@
package me.sashegdev.zernmc.launcher.auth;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import me.sashegdev.zernmc.launcher.utils.Config;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
public class AuthManager {
private static final Path AUTH_FILE = Config.getConfigDir().resolve("auth.json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(15))
.build();
private static AuthSession session = null;
public static boolean loadSavedSession() {
if (!Files.exists(AUTH_FILE)) return false;
try {
String json = Files.readString(AUTH_FILE);
AuthSession loaded = GSON.fromJson(json, AuthSession.class);
if (loaded == null || loaded.accessToken == null) return false;
session = loaded;
if (isAccessTokenExpired()) {
return tryRefresh();
}
return true;
} catch (Exception e) {
return false;
}
}
public static AuthResult login(String username, String password) {
return authRequest("/auth/login", username, password);
}
public static AuthResult register(String username, String password) {
return authRequest("/auth/register", username, password);
}
private static AuthResult authRequest(String endpoint, String username, String password) {
try {
String body = GSON.toJson(new LoginRequest(username, password));
HttpResponse<String> resp = post(endpoint, body);
if (resp.statusCode() == 200) {
session = GSON.fromJson(resp.body(), AuthSession.class);
session.expiresAt = System.currentTimeMillis() / 1000L + session.expiresIn;
saveSession();
return AuthResult.ok();
} else {
return AuthResult.fail(extractError(resp.body()));
}
} catch (Exception e) {
return AuthResult.fail("Ошибка соединения: " + e.getMessage());
}
}
public static void logout() {
if (session != null && session.refreshToken != null) {
try { post("/auth/logout", "{\"refresh_token\":\"" + session.refreshToken + "\"}"); }
catch (Exception ignored) {}
}
session = null;
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {}
}
public static boolean isLoggedIn() {
return session != null && session.accessToken != null;
}
public static String getUsername() {
return session != null ? session.username : "Player";
}
public static String getUuid() {
return session != null ? session.uuid : "00000000-0000-0000-0000-000000000000";
}
public static String getAccessToken() {
if (session == null) return "0";
if (isAccessTokenExpired()) tryRefresh();
return session.accessToken != null ? session.accessToken : "0";
}
private static boolean isAccessTokenExpired() {
if (session == null) return true;
return System.currentTimeMillis() / 1000L >= session.expiresAt - 300;
}
private static boolean tryRefresh() {
if (session == null || session.refreshToken == null) return false;
try {
String body = "{\"refresh_token\":\"" + session.refreshToken + "\"}";
HttpResponse<String> resp = post("/auth/refresh", body);
if (resp.statusCode() == 200) {
AuthSession newSession = GSON.fromJson(resp.body(), AuthSession.class);
newSession.expiresAt = System.currentTimeMillis() / 1000L + newSession.expiresIn;
session = newSession;
saveSession();
return true;
}
} catch (Exception ignored) {}
session = null;
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {}
return false;
}
private static void saveSession() {
try {
Files.createDirectories(AUTH_FILE.getParent());
Files.writeString(AUTH_FILE, GSON.toJson(session));
} catch (IOException e) {
System.err.println(ZAnsi.yellow("Не удалось сохранить сессию: " + e.getMessage()));
}
}
private static HttpResponse<String> post(String endpoint, String jsonBody) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(ZHttpClient.getBaseUrl() + endpoint))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(15))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
return HTTP.send(req, HttpResponse.BodyHandlers.ofString());
}
private static String extractError(String body) {
try {
int idx = body.indexOf("\"detail\"");
if (idx != -1) {
int s = body.indexOf("\"", idx + 9) + 1;
int e = body.indexOf("\"", s);
if (e > s) return body.substring(s, e);
}
} catch (Exception ignored) {}
return "Неизвестная ошибка";
}
// ====================== ВНУТРЕННИЕ КЛАССЫ ======================
public static class AuthSession {
@SerializedName("access_token") public String accessToken;
@SerializedName("refresh_token") public String refreshToken;
@SerializedName("expires_in") public int expiresIn;
public long expiresAt;
public String username;
public String uuid;
}
private static class LoginRequest {
final String username;
final String password;
LoginRequest(String u, String p) { this.username = u; this.password = p; }
}
public static class AuthResult {
public final boolean success;
public final String error;
private AuthResult(boolean s, String e) { success = s; error = e; }
public static AuthResult ok() { return new AuthResult(true, null); }
public static AuthResult fail(String msg) { return new AuthResult(false, msg); }
}
public static boolean hasActivePass() {
if (!isLoggedIn()) return false;
try {
String response = ZHttpClient.get("/auth/pass/my");
// Простая проверка — есть ли хотя бы одна активная проходка
return response.contains("\"is_active\":true");
} catch (Exception e) {
System.err.println("Не удалось проверить проходки: " + e.getMessage());
return false;
}
}
public static String activatePass(String passCode) {
try {
String json = "{\"pass_code\":\"" + passCode.toUpperCase() + "\"}";
HttpResponse<String> resp = post("/auth/pass/activate", json);
if (resp.statusCode() == 200) {
return "Проходка успешно активирована!";
} else {
String error = extractError(resp.body());
return "Ошибка: " + error;
}
} catch (Exception e) {
return "Ошибка соединения: " + e.getMessage();
}
}
}
@@ -1,5 +1,6 @@
package me.sashegdev.zernmc.launcher.menu;
import me.sashegdev.zernmc.launcher.auth.AuthManager;
import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
import me.sashegdev.zernmc.launcher.minecraft.MinecraftLib;
@@ -80,6 +81,32 @@ public class LaunchMenu {
}
private void installServerPack() throws Exception {
if (!AuthManager.hasActivePass()) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightRed("У вас нет активной проходки!"));
System.out.println(ZAnsi.white("Чтобы скачивать сборки с сервера ZernMC, необходимо активировать проходку."));
System.out.println();
System.out.print(ZAnsi.white("Введите код проходки (ZERN-XXXXXXX) или Enter для отмены: "));
String code = Input.readLine();
if (code.isEmpty()) return;
String result = AuthManager.activatePass(code);
System.out.println(ZAnsi.cyan(result));
if (!result.contains("успешно")) {
ConsoleUtils.pause();
return;
}
// Повторная проверка
if (!AuthManager.hasActivePass()) {
System.out.println(ZAnsi.brightRed("Не удалось активировать проходку."));
ConsoleUtils.pause();
return;
}
}
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.cyan("Получение списка доступных сборок с сервера..."));
@@ -502,11 +529,22 @@ public class LaunchMenu {
}
private void launchExistingInstance(Instance instance) {
if (instance.isServerPack() && !AuthManager.hasActivePass()) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightRed("Для запуска серверной сборки требуется активная проходка!"));
ConsoleUtils.pause();
return;
}
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName()));
MinecraftLib lib = new MinecraftLib(instance);
LaunchOptions options = new LaunchOptions();
LaunchOptions options = new LaunchOptions();
// Авторизация Minecraft
options.setUsername(AuthManager.getUsername());
options.setUuid(AuthManager.getUuid());
options.setAccessToken(AuthManager.getAccessToken());
try {
lib.launch(options);
@@ -0,0 +1,167 @@
package me.sashegdev.zernmc.launcher.menu;
import me.sashegdev.zernmc.launcher.auth.AuthManager;
import me.sashegdev.zernmc.launcher.auth.AuthManager.AuthResult;
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
import me.sashegdev.zernmc.launcher.utils.Input;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import java.io.IOException;
import java.util.List;
/**
* Экран входа/регистрации.
* Показывается при старте лаунчера, если нет сохранённой сессии.
*
* show() возвращает true — пользователь вошёл/зарегистрировался
* false — пользователь выбрал выход из лаунчера
*/
public class LoginMenu {
/**
* Главный экран выбора действия.
*/
public boolean show() throws IOException {
while (true) {
ConsoleUtils.clearScreen();
printBanner();
List<String> options = List.of(
"Войти в аккаунт",
"Создать аккаунт",
"Выйти из лаунчера"
);
ArrowMenu menu = new ArrowMenu("Добро пожаловать в ZernMC!", options);
int choice = menu.show();
if (choice == -1 || choice == 2) return false;
boolean success = switch (choice) {
case 0 -> doLogin();
case 1 -> doRegister();
default -> false;
};
if (success) return true;
// Если не успех — покажем меню снова (ошибка уже напечатана внутри методов)
}
}
/**
* Показывается когда пользователь уже вошёл — предлагает выйти из аккаунта.
*/
public void showAccountMenu() throws IOException {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("=== Аккаунт ==="));
System.out.println();
System.out.println(ZAnsi.white(" Игрок: ") + ZAnsi.brightGreen(AuthManager.getUsername()));
System.out.println(ZAnsi.white(" UUID: ") + ZAnsi.cyan(AuthManager.getUuid()));
System.out.println();
List<String> options = List.of(
"Выйти из аккаунта",
"Назад"
);
ArrowMenu menu = new ArrowMenu("Управление аккаунтом", options);
int choice = menu.show();
if (choice == 0) {
AuthManager.logout();
System.out.println(ZAnsi.yellow("Вы вышли из аккаунта."));
ConsoleUtils.pause();
}
}
// ====================== ПРИВАТНЫЕ МЕТОДЫ ======================
private boolean doLogin() throws IOException {
ConsoleUtils.clearScreen();
printBanner();
System.out.println(ZAnsi.cyan(" [ Вход в аккаунт ]"));
System.out.println();
String username = Input.readLine(ZAnsi.white(" Имя пользователя: "));
if (username.isEmpty()) return false;
String password = readPassword(" Пароль: ");
if (password.isEmpty()) return false;
System.out.println();
System.out.print(ZAnsi.cyan(" Выполняем вход..."));
AuthResult result = AuthManager.login(username, password);
if (result.success) {
System.out.println("\r" + ZAnsi.brightGreen(" Добро пожаловать, " + AuthManager.getUsername() + "! "));
ConsoleUtils.pause();
return true;
} else {
System.out.println("\r" + ZAnsi.brightRed(" Ошибка: " + result.error + " "));
ConsoleUtils.pause();
return false;
}
}
private boolean doRegister() throws IOException {
ConsoleUtils.clearScreen();
printBanner();
System.out.println(ZAnsi.cyan(" [ Создание аккаунта ]"));
System.out.println();
System.out.println(ZAnsi.yellow(" Допустимые символы в имени: a-z, A-Z, 0-9, _"));
System.out.println(ZAnsi.yellow(" Длина имени: 3–16 символов | Длина пароля: от 6 символов"));
System.out.println();
String username = Input.readLine(ZAnsi.white(" Имя пользователя: "));
if (username.isEmpty()) return false;
String password = readPassword(" Пароль: ");
if (password.isEmpty()) return false;
String confirm = readPassword(" Повторите пароль: ");
if (!password.equals(confirm)) {
System.out.println(ZAnsi.brightRed("\n Пароли не совпадают!"));
ConsoleUtils.pause();
return false;
}
System.out.println();
System.out.print(ZAnsi.cyan(" Создаём аккаунт..."));
AuthResult result = AuthManager.register(username, password);
if (result.success) {
System.out.println("\r" + ZAnsi.brightGreen(" Аккаунт создан! Добро пожаловать, " + AuthManager.getUsername() + "! "));
ConsoleUtils.pause();
return true;
} else {
System.out.println("\r" + ZAnsi.brightRed(" Ошибка: " + result.error + " "));
ConsoleUtils.pause();
return false;
}
}
/**
* Читаем пароль — стараемся скрыть вывод через Console,
* если недоступно (IDE/терминал без TTY) — читаем обычным способом.
*/
private String readPassword(String prompt) {
java.io.Console console = System.console();
if (console != null) {
char[] chars = console.readPassword(prompt);
return chars != null ? new String(chars) : "";
}
// Fallback: в IDE пароль будет виден
return Input.readLine(prompt);
}
private void printBanner() {
System.out.println(ZAnsi.header("╔══════════════════════════════╗"));
System.out.println(ZAnsi.header("║ ZernMC Launcher ║"));
System.out.println(ZAnsi.header("╚══════════════════════════════╝"));
System.out.println();
}
}
@@ -113,6 +113,10 @@ public class Config {
return CONFIG_DIR.resolve("jre");
}
public static Path getConfigDir() {
return CONFIG_DIR;
}
/**
* Полезная информация для пользователя
*/
@@ -1,10 +1,18 @@
package me.sashegdev.zernmc.launcher.utils;
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
import java.io.IOException;
import java.util.List;
import java.util.Scanner;
/**
* Улучшенный Input с поддержкой кириллицы и confirm через ArrowMenu
*/
public class Input {
private static final Scanner scanner = new Scanner(System.in);
// Используем UTF-8 явно — это помогает на Windows
private static final Scanner scanner = new Scanner(System.in, "UTF-8");
public static String readLine() {
return scanner.nextLine().trim();
@@ -36,9 +44,39 @@ public class Input {
}
}
public static boolean confirm(String prompt) {
System.out.print(prompt + " (да/нет): ");
String answer = scanner.nextLine().trim().toLowerCase();
return answer.equals("да") || answer.equals("y") || answer.equals("yes");
/**
* Новый confirm через ArrowMenu
* @throws IOException
*/
public static boolean confirm(String question) throws IOException {
ConsoleUtils.clearScreen(); // опционально, можно убрать
List<String> options = List.of(
"Да",
"Нет"
);
ArrowMenu menu = new ArrowMenu(question, options);
int choice = menu.show();
return choice == 0; // 0 = "Да"
}
/**
* Альтернативный confirm без очистки экрана
* @throws IOException
*/
public static boolean confirmInline(String question) throws IOException {
List<String> options = List.of("Да", "Нет");
ArrowMenu menu = new ArrowMenu(question, options);
int choice = menu.show();
return choice == 0;
}
/**
* Закрытие сканнера (вызывать при выходе из программы, если нужно)
*/
public static void close() {
scanner.close();
}
}
@@ -26,14 +26,14 @@ public class ZHttpClient {
.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), // Всегда прямое подключение
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),
@@ -41,39 +41,34 @@ public class ZHttpClient {
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;
}
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 final long CHECK_TIMEOUT_MS = 7000; // 7 секунд на проверку
// Статистика
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);
@@ -81,34 +76,33 @@ public class ZHttpClient {
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
ServiceType.ZERN_SERVER,
ServiceType.GOOGLE,
ServiceType.FABRIC_META,
ServiceType.FABRIC_MAVEN,
ServiceType.MOJANG_META,
ServiceType.MOJANG_RESOURCES,
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"));
}
System.out.println(isHealthy ?
ZAnsi.green(" " + service.name() + " - OK") :
ZAnsi.red(" " + service.name() + " - НЕ ДОСТУПЕН (критично!)"));
} else {
if (isHealthy) {
System.out.println(ZAnsi.green(" " + service.name() + " - прямое подключение работает"));
@@ -119,52 +113,70 @@ public class ZHttpClient {
}
}
}
// Проверяем, нужно ли включить глобальный прокси режим
boolean anyCriticalDown = !serviceHealthy.get(ServiceType.ZERN_SERVER);
if (anyCriticalDown) {
if (!serviceHealthy.get(ServiceType.ZERN_SERVER)) {
System.out.println(ZAnsi.brightRed("Критическая ошибка: Zern сервер недоступен!"));
}
proxyTested.set(true);
// Запускаем фоновую проверку
startHealthCheckThread();
printStats();
}
/**
* Проверить здоровье конкретного сервиса
* Принудительная проверка Mojang-сервисов (рекомендуется вызывать перед установкой сборки)
*/
private static boolean checkServiceHealth(ServiceType service) {
if (service.isAlwaysDirect()) {
return checkDirectConnection(service.getBaseUrl());
public static void forceCheckMojangServices() {
System.out.println(ZAnsi.cyan("Принудительная проверка Mojang сервисов..."));
for (ServiceType service : List.of(ServiceType.MOJANG_META, ServiceType.MOJANG_RESOURCES)) {
boolean healthy = checkServiceHealth(service);
serviceHealthy.put(service, healthy);
if (healthy) {
System.out.println(ZAnsi.green(" " + service.name() + " доступен напрямую"));
serviceProxyMode.put(service, false);
serviceFailCount.put(service, 0);
} else {
System.out.println(ZAnsi.yellow(" " + service.name() + " недоступен → прокси режим активирован"));
serviceProxyMode.put(service, true);
serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
}
}
}
private static boolean checkServiceHealth(ServiceType service) {
return checkDirectConnection(service.getBaseUrl());
}
/**
* Проверить прямое подключение к URL
* Улучшенная проверка прямого подключения
*/
private static boolean checkDirectConnection(String url) {
private static boolean checkDirectConnection(String baseUrl) {
String testUrl = baseUrl;
if (baseUrl.contains("piston-meta.mojang.com") || baseUrl.contains("launchermeta.mojang.com")) {
testUrl = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
} else if (baseUrl.contains("resources.download.minecraft.net")) {
testUrl = "https://resources.download.minecraft.net/00/0000000000000000000000000000000000000000";
}
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.uri(URI.create(testUrl))
.timeout(Duration.ofMillis(CHECK_TIMEOUT_MS))
.HEAD()
.GET()
.header("User-Agent", "ZernMC-Launcher/HealthCheck")
.build();
HttpResponse<Void> response = client.send(request, HttpResponse.BodyHandlers.discarding());
return response.statusCode() < 500;
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int code = response.statusCode();
return code == 200 || code == 404; // 404 для ресурсов — нормально
} catch (Exception e) {
return false;
}
}
/**
* Запустить фоновый поток для периодической проверки
*/
private static void startHealthCheckThread() {
Thread healthThread = new Thread(() -> {
while (true) {
@@ -179,30 +191,23 @@ public class ZHttpClient {
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() + " восстановлен, переключен на прямое подключение"));
}
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() + " недоступен, включен прокси режим"));
@@ -210,14 +215,11 @@ public class ZHttpClient {
}
}
}
/**
* Определить тип сервиса по 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"))
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;
@@ -225,108 +227,158 @@ public class ZHttpClient {
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;
if (service == null || service.isAlwaysDirect()) return false;
return serviceProxyMode.getOrDefault(service, false);
}
private static boolean isConnectionError(Throwable e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
String msg = cause.getMessage() != null ? cause.getMessage().toLowerCase() : "";
return cause instanceof java.net.ConnectException ||
cause instanceof java.net.UnknownHostException ||
cause instanceof java.nio.channels.ClosedChannelException ||
msg.contains("connection") ||
msg.contains("timeout") ||
msg.contains("refused") ||
msg.contains("closed");
}
private static void markServiceAsBlocked(String url) {
ServiceType service = detectService(url);
if (service == null || service.isAlwaysDirect()) return;
int fails = serviceFailCount.getOrDefault(service, 0) + 1;
serviceFailCount.put(service, fails);
serviceLastCheckTime.put(service, System.currentTimeMillis());
if (fails >= MAX_FAILS_BEFORE_PROXY && !serviceProxyMode.get(service)) {
serviceProxyMode.put(service, true);
System.out.println(ZAnsi.yellow("[NET] " + service.name() + " заблокирован, переключаемся на прокси"));
}
}
/**
* Получить URL через прокси если нужно
* Универсальный GET с умным прокси + автоматическим fallback
*/
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);
// Попытка прямого подключения
if (!shouldUseProxyForUrl(url)) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(25))
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
directSuccessCount++;
return response.body();
}
if (response.statusCode() >= 400) {
throw new IOException("HTTP " + response.statusCode());
}
} catch (Exception e) {
if (isConnectionError(e)) {
directFailCount++;
markServiceAsBlocked(url);
}
// Если ошибка соединения — пробуем через прокси
}
}
// Через прокси
try {
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
String proxyUrl = BASE_URL + "/download?url=" + encodedUrl;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(30))
.uri(URI.create(proxyUrl))
.timeout(Duration.ofSeconds(40))
.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());
throw new IOException("Proxy HTTP " + response.statusCode());
}
directSuccessCount++;
proxySuccessCount++;
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;
throw new IOException("Не удалось получить данные ни напрямую, ни через прокси: " + e.getMessage(), e);
}
}
/**
* Скачать файл с умным прокси
* Скачивание файла с умным прокси + fallback
*/
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);
if (!shouldUseProxyForUrl(url)) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(40))
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET()
.build();
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() == 200) {
directSuccessCount++;
return;
}
} catch (Exception e) {
if (isConnectionError(e)) {
directFailCount++;
markServiceAsBlocked(url);
}
// fallback на прокси ниже
}
throw e;
}
// Скачивание через прокси
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
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("Proxy download failed: HTTP " + response.statusCode());
}
proxySuccessCount++;
}
// ====================== ОСНОВНЫЕ МЕТОДЫ (СОХРАНЕНЫ) ======================
// ====================== СТАРЫЕ МЕТОДЫ (обновлённые) ======================
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))
@@ -334,20 +386,19 @@ public class ZHttpClient {
.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()
@@ -356,96 +407,64 @@ public class ZHttpClient {
.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);
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);
return parseFabricVersionsFromJson(getWithSmartProxy(url));
}
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;
return new JSONObject(getWithSmartProxy(v.getString("url")));
}
}
if (versionUrl == null) {
throw new IOException("Version " + versionId + " not found");
}
String response = getWithSmartProxy(versionUrl);
return new JSONObject(response);
throw new IOException("Version " + versionId + " not found");
}
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<>();
@@ -457,20 +476,22 @@ public class ZHttpClient {
}
return versions;
}
// ====================== ВСПОМОГАТЕЛЬНЫЕ ======================
public static String getBaseUrl() {
return BASE_URL;
}
public static String getLauncherVersionInfo() throws IOException, InterruptedException {
return get("/launcher/version");
}
public static void forceProxyMode() {
useProxyMode.set(true);
System.out.println(ZAnsi.yellow("Принудительно включен глобальный прокси режим"));
}
public static void disableProxyMode() {
useProxyMode.set(false);
for (ServiceType type : ServiceType.values()) {
@@ -481,23 +502,23 @@ public class ZHttpClient {
}
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("Глобальный прокси: ") + (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("");
String health = serviceHealthy.get(type) ? ZAnsi.green("[+]") : ZAnsi.red("[-]");
System.out.println(ZAnsi.white(" " + type.name() + ": ") + status + " " + health);
}
}