From fc748a744ceb463db548991609169b82a670950270ebcb25bc348b1d4e47c752 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 5 Dec 2025 10:56:34 +0300 Subject: [PATCH] 04 12 25 --- build.gradle | 2 + settings.gradle | 1 + shine-server-crypto/src/concat_to_file.sh | 16 ++ .../java/shine/db/SqliteDbController.java | 1 + shine-server-geo/build.gradle | 25 ++ .../main/java/shine.geo/GeoLookupService.java | 137 ++++++++++ .../java/shine.geo/GeoLookupTestMain.java | 37 +++ shine-server-net-protocol/build.gradle | 2 + .../ws_protocol/JSON/ConnectionContext.java | 99 +++----- .../ws_protocol/JSON/JsonHandlerRegistry.java | 8 +- .../JSON/JsonInboundProcessor.java | 163 +++++++----- .../Auth/NetAuthSessionNewStep2Request.java | 56 +++++ .../Auth/NetAuthSessionNewStep2Response.java | 29 +++ .../JSON/entyties/NetExceptionResponse.java | 22 +- .../auth/NetAuthSessionNewStep1Handler.java | 13 +- .../auth/NetAuthSessionNewStep2Handler.java | 190 ++++++++++++++ .../auth/NetSessionRefreshHandler.java | 54 ++-- .../tempToTest/NetAddUserHandler.java | 10 +- .../utils/NetExceptionResponseFactory.java | 28 ++- .../java/{ => Test}/TestJsonWsClient2.java | 3 +- .../java/Test/Test_AddUser_FirstAuth.java | 236 ++++++++++++++++++ .../java/Test/Test_SessionRefreshClient.java | 109 ++++++++ src/main/java/TestJsonWsClient.java | 135 ---------- 23 files changed, 1069 insertions(+), 307 deletions(-) create mode 100755 shine-server-crypto/src/concat_to_file.sh create mode 100644 shine-server-geo/build.gradle create mode 100644 shine-server-geo/src/main/java/shine.geo/GeoLookupService.java create mode 100644 shine-server-geo/src/main/java/shine.geo/GeoLookupTestMain.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Response.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java rename src/main/java/{ => Test}/TestJsonWsClient2.java (99%) create mode 100644 src/main/java/Test/Test_AddUser_FirstAuth.java create mode 100644 src/main/java/Test/Test_SessionRefreshClient.java delete mode 100644 src/main/java/TestJsonWsClient.java diff --git a/build.gradle b/build.gradle index bf9e86f..71ee531 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,8 @@ dependencies { implementation project(':shine-server-config') // модуль настроек из application.properties + implementation project('shine-server-geo') // модуль для определения геолокации по IP + implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией implementation project(':shine-server-blockchain') // модуль для работы с блокчейном implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД diff --git a/settings.gradle b/settings.gradle index 9988e51..4795f39 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'shine-server-server' include 'shine-server-config' +include 'shine-server-geo' include 'shine-server-crypto' include 'shine-server-blockchain' include 'shine-server-db' diff --git a/shine-server-crypto/src/concat_to_file.sh b/shine-server-crypto/src/concat_to_file.sh new file mode 100755 index 0000000..901712c --- /dev/null +++ b/shine-server-crypto/src/concat_to_file.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUTFILE="all_files.txt" + +# очищаем или создаём файл +: > "$OUTFILE" + +# собрать только *.java файлы и вывести их содержимое в файл +find . -type f -name "*.java" | sort | while read -r f; do + cat "$f" >> "$OUTFILE" + echo >> "$OUTFILE" # пустая строка-разделитель +done + +echo "Готово! Все .java файлы собраны в $OUTFILE" + diff --git a/shine-server-db/src/main/java/shine/db/SqliteDbController.java b/shine-server-db/src/main/java/shine/db/SqliteDbController.java index 11399be..4667e8f 100644 --- a/shine-server-db/src/main/java/shine/db/SqliteDbController.java +++ b/shine-server-db/src/main/java/shine/db/SqliteDbController.java @@ -1,6 +1,7 @@ package shine.db; import shine.db.dao.ActiveSessionsDAO; +import shine.db.entities.ActiveSession; import utils.config.AppConfig; import java.nio.file.Files; diff --git a/shine-server-geo/build.gradle b/shine-server-geo/build.gradle new file mode 100644 index 0000000..d6640b4 --- /dev/null +++ b/shine-server-geo/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' +} + +group = 'shine' +version = '1.0.0' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' // json +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} diff --git a/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java b/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java new file mode 100644 index 0000000..34ef1fd --- /dev/null +++ b/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java @@ -0,0 +1,137 @@ +package shine.geo; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +/** + * Сервис для геолокации по IP. + * + * Основной метод: + * resolveCountryCityOrIp(ip) -> "Country, City" или исходный ip, если не удалось. + */ +public final class GeoLookupService { + + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + // Сервис геолокации. Сейчас ip-api.com, можно потом вынести в конфиг. + private static final String GEO_API_URL = "http://ip-api.com/json/"; + + // Сервис для получения собственного внешнего IP + private static final String PUBLIC_IP_URL = "https://api.ipify.org"; + + private GeoLookupService() { + // utility-класс + } + + /** + * Возвращает строку вида "Country, City" по IP. + * Если запрос не удался, возвращает исходный ip. + */ + public static String resolveCountryCityOrIp(String ip) { + // На всякий случай простая защита от private/локальных IP (они всё равно не определяются) + if (isPrivateOrLocalIp(ip)) { + return ip; + } + + try { + String url = GEO_API_URL + ip + "?fields=status,country,city,message"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = HTTP_CLIENT.send( + request, + HttpResponse.BodyHandlers.ofString() + ); + + if (response.statusCode() != 200) { + return ip; + } + + JsonNode root = JSON_MAPPER.readTree(response.body()); + String status = root.path("status").asText(); + if (!"success".equals(status)) { + // Например: {"status":"fail","message":"private range"} + return ip; + } + + String country = root.path("country").asText(null); + String city = root.path("city").asText(null); + + if (country == null && city == null) { + return ip; + } + + // Собираем аккуратную строку + if (country != null && city != null) { + return country + ", " + city; + } else if (country != null) { + return country; + } else { + return city; + } + } catch (IOException | InterruptedException e) { + // В боевом коде можно логировать + return ip; + } + } + + /** + * Пытается получить внешний IP текущей машины через HTTP-сервис. + * В случае ошибки возвращает fallbackIp. + */ + public static String fetchPublicIpOrDefault(String fallbackIp) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(PUBLIC_IP_URL)) + .GET() + .build(); + + HttpResponse response = HTTP_CLIENT.send( + request, + HttpResponse.BodyHandlers.ofString() + ); + + if (response.statusCode() != 200) { + return fallbackIp; + } + + String body = response.body(); + if (body == null || body.isBlank()) { + return fallbackIp; + } + + return body.trim(); + } catch (IOException | InterruptedException e) { + // В боевом коде можно логировать + return fallbackIp; + } + } + + /** + * Примитивная проверка на частные и локальные IP. + * Для внешней геолокации они бесполезны. + */ + private static boolean isPrivateOrLocalIp(String ip) { + if (ip == null) return true; + + ip = ip.trim(); + + return ip.startsWith("10.") + || ip.startsWith("192.168.") + || ip.startsWith("127.") + || ip.startsWith("0.") + || ip.startsWith("169.254.") + // Диапазон 172.16.0.0 – 172.31.255.255 + || ip.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"); + } +} diff --git a/shine-server-geo/src/main/java/shine.geo/GeoLookupTestMain.java b/shine-server-geo/src/main/java/shine.geo/GeoLookupTestMain.java new file mode 100644 index 0000000..230ee63 --- /dev/null +++ b/shine-server-geo/src/main/java/shine.geo/GeoLookupTestMain.java @@ -0,0 +1,37 @@ +package shine.geo; + +/** + * Тестовый запуск геолокации. + * + * Логика: + * 1) Если в args[0] передан IP — используем его. + * 2) Иначе пробуем узнать внешний IP текущей машины. + * 3) Если не удалось — берём константу TEST_IP. + * 4) Вызываем GeoLookupService.resolveCountryCityOrIp(...) и печатаем результат. + */ +public class GeoLookupTestMain { + + // Константа на случай, если не удалось узнать внешний IP. + private static final String TEST_IP = "8.8.8.8"; + + public static void main(String[] args) { + String ip; + + if (args.length > 0 && args[0] != null && !args[0].isBlank()) { + ip = args[0].trim(); + System.out.println("Используем IP из аргумента: " + ip); + } else { + // Пытаемся узнать внешний IP + String detectedIp = GeoLookupService.fetchPublicIpOrDefault(TEST_IP); + if (TEST_IP.equals(detectedIp)) { + System.out.println("Не удалось определить внешний IP, используем тестовый: " + TEST_IP); + } else { + System.out.println("Определён внешний IP: " + detectedIp); + } + ip = detectedIp; + } + + String result = GeoLookupService.resolveCountryCityOrIp(ip); + System.out.println("Результат геолокации: " + result); + } +} diff --git a/shine-server-net-protocol/build.gradle b/shine-server-net-protocol/build.gradle index f7c4f3a..09693cf 100644 --- a/shine-server-net-protocol/build.gradle +++ b/shine-server-net-protocol/build.gradle @@ -22,6 +22,8 @@ dependencies { implementation 'ch.qos.logback:logback-classic:1.5.6' implementation project(':shine-server-config') // модуль с настройками + implementation project(':shine-server-crypto') // модуль сервера для работы с криптографией + implementation project(':shine-server-blockchain') // модуль для работы с блокчейном implementation project(':shine-server-db') // модуль для работы с БД содержит и сущности из БД и саму работу с БД } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java index 80adc00..a8507da 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java @@ -1,5 +1,8 @@ package server.logic.ws_protocol.JSON; +import shine.db.entities.SolanaUser; +import shine.db.entities.ActiveSession; + /** * ConnectionContext — контекст состояния одного WebSocket-соединения. * Живёт ровно столько же, сколько живёт подключение. @@ -7,41 +10,49 @@ package server.logic.ws_protocol.JSON; public class ConnectionContext { // Статусы аутентификации - public static final int AUTH_STATUS_NONE = 0; // ананимный или не авторизованный пользователь + public static final int AUTH_STATUS_NONE = 0; // анонимный или не авторизованный пользователь public static final int AUTH_STATUS_USER = 1; // авторизованный пользователь -// public static final int AUTH_STATUS_ANON = 2; // анонимный (зарезервировано на будущее) - private String login; - private Long loginId; + // Полный пользователь из БД (solana_users) + private SolanaUser solanaUser; + + // Активная сессия из БД (active_sessions) + private ActiveSession activeSession; private Long sessionId; private String sessionPwd; - // Данные пользователя / блокчейна - private Long bchId; - private String pubkey0; - private String pubkey1; - private Integer bchLimit; - private int authenticationStatus = AUTH_STATUS_NONE; - // --- getters / setters --- + // --- SolanaUser / ActiveSession --- - public String getLogin() { - return login; + public SolanaUser getSolanaUser() { + return solanaUser; } - public void setLogin(String login) { - this.login = login; + public void setSolanaUser(SolanaUser solanaUser) { + this.solanaUser = solanaUser; + } + + public ActiveSession getActiveSession() { + return activeSession; + } + + public void setActiveSession(ActiveSession activeSession) { + this.activeSession = activeSession; + } + + // --- Удобные геттеры для логина --- + + public String getLogin() { + return solanaUser != null ? solanaUser.getLogin() : null; } public Long getLoginId() { - return loginId; + return solanaUser != null ? solanaUser.getLoginId() : null; } - public void setLoginId(Long loginId) { - this.loginId = loginId; - } + // --- sessionId / sessionPwd --- public Long getSessionId() { return sessionId; @@ -59,37 +70,7 @@ public class ConnectionContext { this.sessionPwd = sessionPwd; } - public Long getBchId() { - return bchId; - } - - public void setBchId(Long bchId) { - this.bchId = bchId; - } - - public String getPubkey0() { - return pubkey0; - } - - public void setPubkey0(String pubkey0) { - this.pubkey0 = pubkey0; - } - - public String getPubkey1() { - return pubkey1; - } - - public void setPubkey1(String pubkey1) { - this.pubkey1 = pubkey1; - } - - public Integer getBchLimit() { - return bchLimit; - } - - public void setBchLimit(Integer bchLimit) { - this.bchLimit = bchLimit; - } + // --- auth status --- public int getAuthenticationStatus() { return authenticationStatus; @@ -108,29 +89,21 @@ public class ConnectionContext { } public void reset() { - login = null; - loginId = null; + solanaUser = null; + activeSession = null; + sessionId = null; sessionPwd = null; - bchId = null; - pubkey0 = null; - pubkey1 = null; - bchLimit = null; - authenticationStatus = AUTH_STATUS_NONE; } @Override public String toString() { return "ConnectionContext{" + - "login='" + login + '\'' + - ", loginId=" + loginId + + "login='" + getLogin() + '\'' + + ", loginId=" + getLoginId() + ", sessionId=" + sessionId + - ", bchId=" + bchId + - ", pubkey0='" + pubkey0 + '\'' + - ", pubkey1='" + pubkey1 + '\'' + - ", bchLimit=" + bchLimit + ", authenticationStatus=" + authenticationStatus + '}'; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index 8095bf0..7052bdb 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -2,9 +2,11 @@ package server.logic.ws_protocol.JSON; import server.logic.ws_protocol.JSON.entyties.*; import server.logic.ws_protocol.JSON.entyties.Auth.NetAuthSessionNewStep1Request; +import server.logic.ws_protocol.JSON.entyties.Auth.NetAuthSessionNewStep2Request; import server.logic.ws_protocol.JSON.entyties.Auth.NetSessionRefreshRequest; import server.logic.ws_protocol.JSON.handlers.*; import server.logic.ws_protocol.JSON.entyties.tempToTest.NetAddUserRequest; +import server.logic.ws_protocol.JSON.handlers.auth.NetAuthSessionNewStep2Handler; import server.logic.ws_protocol.JSON.handlers.tempToTest.NetAddUserHandler; import server.logic.ws_protocol.JSON.handlers.auth.NetAuthSessionNewStep1Handler; import server.logic.ws_protocol.JSON.handlers.auth.NetSessionRefreshHandler; @@ -25,14 +27,16 @@ public final class JsonHandlerRegistry { private static final Map HANDLERS = Map.of( "SessionRefresh", new NetSessionRefreshHandler(), "AddUser", new NetAddUserHandler(), - "AuthSessionNewStep1", new NetAuthSessionNewStep1Handler() + "AuthSessionNewStep1", new NetAuthSessionNewStep1Handler(), + "AuthSessionNewStep2", new NetAuthSessionNewStep2Handler() // сюда потом добавишь другие операции ); private static final Map> REQUEST_TYPES = Map.of( "SessionRefresh", NetSessionRefreshRequest.class, "AddUser", NetAddUserRequest.class, - "AuthSessionNewStep1", NetAuthSessionNewStep1Request.class + "AuthSessionNewStep1", NetAuthSessionNewStep1Request.class, + "AuthSessionNewStep2", NetAuthSessionNewStep2Request.class ); private JsonHandlerRegistry() { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonInboundProcessor.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonInboundProcessor.java index 31eba19..3788172 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonInboundProcessor.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonInboundProcessor.java @@ -1,78 +1,95 @@ package server.logic.ws_protocol.JSON; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.entyties.NetExceptionResponse; import server.logic.ws_protocol.JSON.entyties.NetRequest; import server.logic.ws_protocol.JSON.entyties.NetResponse; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; import java.util.Map; /** - * JsonInboundProcessor — отдельный класс для обработки JSON-сообщений. + * JsonInboundProcessor — обработка JSON-сообщений. * - * 1) Парсит общий пакет (op, requestId, payload). + * 1) Парсит общий пакет (op, requestId,...). * 2) По op выбирает класс запроса и хэндлер. * 3) Маппит JSON → NetRequest через ObjectMapper. * 4) Вызывает хэндлер, получает NetResponse. - * 5) Собирает JSON-ответ и возвращает строкой. + * 5) Собирает JSON-ответ: + * { + * "op": ..., + * "requestId": ..., + * "status": ..., + * "payload": { все поля response, кроме op/requestId/status/payload } + * } */ public final class JsonInboundProcessor { private static final Logger log = LoggerFactory.getLogger(JsonInboundProcessor.class); - private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + private static final ObjectMapper JSON_MAPPER = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL); - /** - * op → хэндлер. - * Регистрация вынесена в JsonHandlerRegistry. - */ private static final Map JSON_HANDLERS = JsonHandlerRegistry.getHandlers(); - /** - * op → класс запроса. - */ private static final Map> JSON_REQUEST_TYPES = JsonHandlerRegistry.getRequestTypes(); private JsonInboundProcessor() {} - /** - * Обработка входящего JSON-сообщения. - * - * @param json исходная строка от клиента - * @param ctx контекст текущего WebSocket-соединения - * @return JSON-строка ответа - */ public static String processJson(String json, ConnectionContext ctx) { + String op = null; + String requestId = null; + try { if (json == null || json.isBlank()) { - return buildErrorJson(null, null, WireCodes.Status.BAD_REQUEST, - "EMPTY_JSON", "Пустое JSON-сообщение"); + NetExceptionResponse err = NetExceptionResponseFactory.error( + null, + null, + WireCodes.Status.BAD_REQUEST, + "EMPTY_JSON", + "Пустое JSON-сообщение" + ); + return writeResponse(err); } // 1. Парсим общий пакет JsonNode root = JSON_MAPPER.readTree(json); // 2. op и requestId - String op = getTextOrNull(root, "op"); - if (op == null || op.isEmpty()) { - return buildErrorJson(null, null, WireCodes.Status.BAD_REQUEST, - "NO_OP", "Поле 'op' отсутствует или пустое"); - } + op = getTextOrNull(root, "op"); + requestId = getTextOrNull(root, "requestId"); - String requestId = getTextOrNull(root, "requestId"); + if (op == null || op.isEmpty()) { + NetExceptionResponse err = NetExceptionResponseFactory.error( + null, + requestId, + WireCodes.Status.BAD_REQUEST, + "NO_OP", + "Поле 'op' отсутствует или пустое" + ); + return writeResponse(err); + } JsonMessageHandler handler = JSON_HANDLERS.get(op); Class reqClass = JSON_REQUEST_TYPES.get(op); if (handler == null || reqClass == null) { - return buildErrorJson(op, requestId, WireCodes.Status.BAD_REQUEST, - "UNKNOWN_OP", "Неизвестная операция: " + op); + NetExceptionResponse err = NetExceptionResponseFactory.error( + op, + requestId, + WireCodes.Status.BAD_REQUEST, + "UNKNOWN_OP", + "Неизвестная операция: " + op + ); + return writeResponse(err); } // 3. Маппим JSON → нужный NetRequest @@ -80,43 +97,42 @@ public final class JsonInboundProcessor { NetResponse response; - // 4. Трай-кэтч вокруг хэндлера (важно!) + // 4. Трай-кэтч вокруг хэндлера try { response = handler.handle(request, ctx); } catch (Exception handlerError) { log.error("💥 Ошибка внутри хэндлера '{}'", op, handlerError); - return buildErrorJson(op, requestId, + NetExceptionResponse err = NetExceptionResponseFactory.error( + op, + requestId, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_HANDLER_ERROR", - "Неожиданная ошибка при обработке операции: " + op); + "Неожиданная ошибка при обработке операции: " + op + ); + return writeResponse(err); } - // Если хэндлер не выставил op/requestId + // На всякий случай: если хэндлер не выставил op/requestId if (response.getOp() == null) response.setOp(op); if (response.getRequestId() == null) response.setRequestId(requestId); - // 5. Формируем JSON - ObjectNode out = JSON_MAPPER.createObjectNode(); - out.put("op", response.getOp()); - out.put("requestId", response.getRequestId()); - out.put("status", response.getStatus()); - - if (response.getPayload() != null) { - out.set("payload", JSON_MAPPER.valueToTree(response.getPayload())); - } else { - out.putNull("payload"); - } - - return JSON_MAPPER.writeValueAsString(out); + // 5. Универсальная сборка ответа + return writeResponse(response); } catch (Exception e) { log.error("Ошибка при обработке JSON-сообщения", e); - return buildErrorJson("Unknown", null, WireCodes.Status.INTERNAL_ERROR, - "INTERNAL_ERROR", "Внутренняя ошибка сервера"); + NetExceptionResponse err = NetExceptionResponseFactory.error( + op != null ? op : "Unknown", + requestId, + WireCodes.Status.INTERNAL_ERROR, + "INTERNAL_ERROR", + "Внутренняя ошибка сервера" + ); + return writeResponse(err); } } - // --- helper'ы --- + // --- helpers --- private static String getTextOrNull(JsonNode node, String field) { if (node == null || !node.has(field) || node.get(field).isNull()) return null; @@ -124,32 +140,51 @@ public final class JsonInboundProcessor { } /** - * Генерация JSON-ошибки + * Унифицированная сериализация любого NetResponse в формат: + * { + * "op": ..., + * "requestId": ..., + * "status": ..., + * "payload": { ... } + * } */ - private static String buildErrorJson(String op, - String requestId, - int status, - String errorCode, - String errorMessage) { + private static String writeResponse(NetResponse response) { try { - ObjectNode root = JSON_MAPPER.createObjectNode(); + // Конвертируем полный объект ответа в ObjectNode + ObjectNode full = JSON_MAPPER.convertValue(response, ObjectNode.class); + // То, что должно остаться наверху: + String op = full.hasNonNull("op") ? full.get("op").asText() : null; + String requestId = full.hasNonNull("requestId") ? full.get("requestId").asText() : null; + int status = full.hasNonNull("status") ? full.get("status").asInt() : 0; + + // Удаляем базовые поля и payload из "полного" объекта, + // всё остальное отправляем внутрь payload. + full.remove("op"); + full.remove("requestId"); + full.remove("status"); + full.remove("payload"); + + ObjectNode root = JSON_MAPPER.createObjectNode(); if (op != null) root.put("op", op); else root.putNull("op"); if (requestId != null) root.put("requestId", requestId); else root.putNull("requestId"); - root.put("status", status); - ObjectNode payload = root.putObject("payload"); - payload.put("code", errorCode); - payload.put("message", errorMessage); + // payload — это всё, что осталось от full (может быть пустым объектом {}) + root.set("payload", full); return JSON_MAPPER.writeValueAsString(root); } catch (Exception e) { - return "{\"op\":\"" + (op != null ? op : "") + - "\",\"requestId\":\"" + (requestId != null ? requestId : "") + - "\",\"status\":" + status + - ",\"payload\":{\"code\":\"" + errorCode + - "\",\"message\":\"" + errorMessage + "\"}}"; + // Совсем аварийный случай — сериализация ответа сломалась. + return "{\"op\":\"" + safe(response.getOp()) + + "\",\"requestId\":\"" + safe(response.getRequestId()) + + "\",\"status\":" + response.getStatus() + + ",\"payload\":{\"code\":\"SERIALIZATION_ERROR\",\"message\":\"" + + "Ошибка сериализации ответа\"}}"; } } + + private static String safe(String s) { + return s != null ? s : ""; + } } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Request.java new file mode 100644 index 0000000..41ef7a5 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Request.java @@ -0,0 +1,56 @@ +package server.logic.ws_protocol.JSON.entyties.Auth; + +import server.logic.ws_protocol.JSON.entyties.NetRequest; + +/** + * Шаг 2 авторизации: клиент подтверждает владение ключом. + * + * JSON: + * { + * "op": "AuthSessionNewStep2", + * "requestId": "...", + * "loginId": 100211, + * "sigNum": 0, // номер подписи: 0 или 1 + * "timeMs": 1733310000000, // время в миллисекундах с 1970-01-01 + * "signatureB64": "..." // подпись base64 от строки loginId+timeMs+sessionPwd + * } + */ +public class NetAuthSessionNewStep2Request extends NetRequest { + + private long loginId; + private int sigNum; // 0 или 1 + private long timeMs; // миллисекунды с 1970 + private String signatureB64; + + public long getLoginId() { + return loginId; + } + + public void setLoginId(long loginId) { + this.loginId = loginId; + } + + public int getSigNum() { + return sigNum; + } + + public void setSigNum(int sigNum) { + this.sigNum = sigNum; + } + + public long getTimeMs() { + return timeMs; + } + + public void setTimeMs(long timeMs) { + this.timeMs = timeMs; + } + + public String getSignatureB64() { + return signatureB64; + } + + public void setSignatureB64(String signatureB64) { + this.signatureB64 = signatureB64; + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Response.java new file mode 100644 index 0000000..5cda6a5 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/Auth/NetAuthSessionNewStep2Response.java @@ -0,0 +1,29 @@ +package server.logic.ws_protocol.JSON.entyties.Auth; + +import server.logic.ws_protocol.JSON.entyties.NetResponse; + +/** + * Ответ на AuthSessionNewStep2. + * + * Успешный JSON: + * { + * "op": "AuthSessionNewStep2", + * "requestId": "...", + * "status": 200, + * "payload": { + * "sessionId": 1234567890 + * } + * } + */ +public class NetAuthSessionNewStep2Response extends NetResponse { + + private Long sessionId; + + public Long getSessionId() { + return sessionId; + } + + public void setSessionId(Long sessionId) { + this.sessionId = sessionId; + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/NetExceptionResponse.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/NetExceptionResponse.java index af80a82..88c0635 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/NetExceptionResponse.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/NetExceptionResponse.java @@ -3,12 +3,30 @@ package server.logic.ws_protocol.JSON.entyties; /** * Ответ с ошибкой (любой отказ). * - * В payload лежит: + * В payload будет: * { * "code": "...", * "message": "..." * } */ public class NetExceptionResponse extends NetResponse { - // Ничего дополнительного: код/текст ошибки лежат в payload (Map или DTO). + + private String code; + private String message; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep1Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep1Handler.java index 6371d01..8a2f87e 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep1Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep1Handler.java @@ -11,7 +11,6 @@ import shine.db.dao.SolanaUsersDAO; import shine.db.entities.SolanaUser; import java.security.SecureRandom; -import java.util.Map; public class NetAuthSessionNewStep1Handler implements JsonMessageHandler { @@ -55,16 +54,10 @@ public class NetAuthSessionNewStep1Handler implements JsonMessageHandler { ); } - // 3) Заполняем контекст полями пользователя - ctx.setLogin(solanaUser.getLogin()); - ctx.setLoginId(solanaUser.getLoginId()); - ctx.setBchId(solanaUser.getBchId()); - ctx.setPubkey0(solanaUser.getPubkey0()); - ctx.setPubkey1(solanaUser.getPubkey1()); - ctx.setBchLimit(solanaUser.getBchLimit()); + // 3) Заполняем контекст целиком пользователем + ctx.setSolanaUser(solanaUser); // 4) Генерируем надёжный sessionPwd - // SecureRandom + время → достаточно String sessionPwd = Long.toHexString(System.nanoTime()) + Long.toHexString(RANDOM.nextLong()); @@ -75,7 +68,7 @@ public class NetAuthSessionNewStep1Handler implements JsonMessageHandler { resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - resp.setPayload(Map.of("sessionPwd", sessionPwd)); + resp.setSessionPwd(sessionPwd); // 🔴 Больше не трогаем payload return resp; } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java new file mode 100644 index 0000000..7430bba --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetAuthSessionNewStep2Handler.java @@ -0,0 +1,190 @@ +package server.logic.ws_protocol.JSON.handlers.auth; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.NetRequest; +import server.logic.ws_protocol.JSON.entyties.NetResponse; +import server.logic.ws_protocol.JSON.entyties.Auth.NetAuthSessionNewStep2Request; +import server.logic.ws_protocol.JSON.entyties.Auth.NetAuthSessionNewStep2Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.dao.ActiveSessionsDAO; +import shine.db.entities.ActiveSession; +import shine.db.entities.SolanaUser; +import utils.crypto.Ed25519Util; + +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.util.Base64; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Шаг 2 авторизации: проверка подписи и создание сессии. + * + * Клиент присылает: + * - loginId + * - sigNum (0 или 1) + * - timeMs + * - signatureB64 от строки (loginId + timeMs + sessionPwd) + */ +public class NetAuthSessionNewStep2Handler implements JsonMessageHandler { + + private static final Logger log = LoggerFactory.getLogger(NetAuthSessionNewStep2Handler.class); + + @Override + public NetResponse handle(NetRequest baseReq, ConnectionContext ctx) throws Exception { + NetAuthSessionNewStep2Request req = (NetAuthSessionNewStep2Request) baseReq; + + // --- базовые проверки контекста --- + if (ctx == null || ctx.getSolanaUser() == null || ctx.getSessionPwd() == null) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "NO_STEP1_CONTEXT", + "Шаг 1 авторизации не был корректно выполнен для данного соединения" + ); + } + + if (!ctx.isAnonymous()) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "ALREADY_AUTHED", + "Пользователь уже авторизован по текущему соединению" + ); + } + + SolanaUser user = ctx.getSolanaUser(); + long reqLoginId = req.getLoginId(); + Long ctxLoginId = user.getLoginId(); + + if (ctxLoginId == null || ctxLoginId != reqLoginId) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.UNVERIFIED, + "LOGIN_ID_MISMATCH", + "loginId в запросе не совпадает с пользователем из шага 1" + ); + } + + int sigNum = req.getSigNum(); + if (sigNum != 0 && sigNum != 1) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_SIG_NUM", + "Номер подписи должен быть 0 или 1" + ); + } + + String signatureB64 = req.getSignatureB64(); + if (signatureB64 == null || signatureB64.isBlank()) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "EMPTY_SIGNATURE", + "Пустая цифровая подпись" + ); + } + + // --- выбираем публичный ключ по sigNum --- + String pubKeyB64 = (sigNum == 0) ? user.getPubkey0() : user.getPubkey1(); + if (pubKeyB64 == null || pubKeyB64.isBlank()) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "NO_PUBKEY", + "Отсутствует публичный ключ для выбранного номера подписи" + ); + } + + byte[] publicKey32; + byte[] signature64; + try { + publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64); + signature64 = Base64.getDecoder().decode(signatureB64); + } catch (IllegalArgumentException ex) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_BASE64", + "Некорректный формат Base64 для ключа или подписи" + ); + } + + // --- собираем строку для подписи: loginId + timeMs + sessionPwd --- + long timeMs = req.getTimeMs(); + String preimageStr = String.valueOf(reqLoginId) + timeMs + ctx.getSessionPwd(); + byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); + + boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32); + if (!sigOk) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.UNVERIFIED, + "BAD_SIGNATURE", + "Подпись не прошла проверку" + ); + } + + // --- создаём уникальный sessionId и записываем в БД --- + ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); + long sessionId; + ActiveSession activeSession; + + try { + sessionId = generateUniqueSessionId(dao); + long nowMs = System.currentTimeMillis(); + + activeSession = new ActiveSession( + sessionId, + ctx.getSessionPwd(), + reqLoginId, + nowMs, + (short) sigNum, // pubkeyNum + null, // pushEndpoint + null, // pushP256dhKey + null // pushAuthKey + ); + + dao.insert(activeSession); + } catch (SQLException e) { + log.error("Ошибка БД при создании новой сессии для loginId={}", reqLoginId, e); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.SERVER_DATA_ERROR, + "DB_ERROR_SESSION_CREATE", + "Ошибка БД при создании сессии" + ); + } + + // --- обновляем контекст --- + ctx.setActiveSession(activeSession); + ctx.setSessionId(sessionId); + ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); + + // --- формируем ответ --- + NetAuthSessionNewStep2Response resp = new NetAuthSessionNewStep2Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + resp.setSessionId(sessionId); // попадёт в payload.sessionId + return resp; + } + + /** + * Генерация уникального sessionId с проверкой в БД. + */ + private long generateUniqueSessionId(ActiveSessionsDAO dao) throws SQLException { + for (int i = 0; i < 10; i++) { + long candidate = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); + ActiveSession existing = dao.getBySessionId(candidate); + if (existing == null) { + return candidate; + } + } + throw new SQLException("Не удалось сгенерировать уникальный sessionId за разумное число попыток"); + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetSessionRefreshHandler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetSessionRefreshHandler.java index d851763..e490bb4 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetSessionRefreshHandler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/NetSessionRefreshHandler.java @@ -1,5 +1,7 @@ package server.logic.ws_protocol.JSON.handlers.auth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.entyties.NetRequest; import server.logic.ws_protocol.JSON.entyties.NetResponse; @@ -9,23 +11,19 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; import shine.db.dao.ActiveSessionsDAO; +import shine.db.dao.SolanaUsersDAO; import shine.db.entities.ActiveSession; +import shine.db.entities.SolanaUser; import java.sql.SQLException; /** * Хэндлер SessionRefresh. - * - * Логика: - * - берём sessionId и sessionPwd из запроса; - * - ищем сессию в БД; - * - если не нашли или пароль не совпал → NetExceptionResponse; - * - если всё ок: - * * обновляем ConnectionContext (sessionId, sessionPwd, статус USER); - * * возвращаем NetSessionRefreshResponse со статусом 200. */ public class NetSessionRefreshHandler implements JsonMessageHandler { + private static final Logger log = LoggerFactory.getLogger(NetSessionRefreshHandler.class); + @Override public NetResponse handle(NetRequest request, ConnectionContext ctx) throws Exception { NetSessionRefreshRequest req = (NetSessionRefreshRequest) request; @@ -42,12 +40,12 @@ public class NetSessionRefreshHandler implements JsonMessageHandler { ); } - ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); + ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance(); ActiveSession session; try { - session = dao.getBySessionId(sessionId); + session = sessionsDao.getBySessionId(sessionId); } catch (SQLException e) { - // Ошибка БД → внутренняя ошибка сервера + log.error("Ошибка БД при поиске сессии sessionId={}", sessionId, e); return NetExceptionResponseFactory.error( req, WireCodes.Status.SERVER_DATA_ERROR, @@ -75,20 +73,48 @@ public class NetSessionRefreshHandler implements JsonMessageHandler { ); } + // --- достаём пользователя по loginId из сессии --- + SolanaUser solanaUser = null; + Long loginId = null; + try { + loginId = session.getLoginId(); + if (loginId != null) { + SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); + solanaUser = usersDao.getByLoginId(loginId); + } + } catch (SQLException e) { + log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.SERVER_DATA_ERROR, + "DB_ERROR_USER_LOOKUP", + "Ошибка доступа к базе данных при получении пользователя для сессии" + ); + } + + if (loginId != null && solanaUser == null) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.UNVERIFIED, + "USER_NOT_FOUND_FOR_SESSION", + "Пользователь для данной сессии не найден" + ); + } + // Всё хорошо — обновляем контекст соединения if (ctx != null) { + ctx.setActiveSession(session); + ctx.setSolanaUser(solanaUser); ctx.setSessionId(sessionId); ctx.setSessionPwd(sessionPwd); - // Если потом добавишь в ActiveSession login / loginId — можно здесь и их проставлять ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); } - // И возвращаем OK без доп. данных + // И возвращаем OK без доп. полей (payload будет {}). NetSessionRefreshResponse resp = new NetSessionRefreshResponse(); resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - resp.setPayload(null); // или Map.of("ok", true) return resp; } } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/NetAddUserHandler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/NetAddUserHandler.java index 7f3e4f8..90b1dc1 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/NetAddUserHandler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/NetAddUserHandler.java @@ -16,13 +16,7 @@ import shine.db.entities.SolanaUser; import java.sql.SQLException; /** - * Временный Хэндлер AddUser. Используется для тестовой регистрации!!!!!!!! - * - * Логика: - * - берём login, loginId, bchId, pubkey0, pubkey1, bchLimit; - * - создаём SolanaUser и вставляем через SolanaUsersDAO; - * - если всё ОК → NetAddUserResponse со статусом 200; - * - если ошибка БД или некорректные данные → NetExceptionResponse. + * Временный хэндлер AddUser (тестовая регистрация). */ public class NetAddUserHandler implements JsonMessageHandler { @@ -64,7 +58,7 @@ public class NetAddUserHandler implements JsonMessageHandler { resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); - resp.setPayload(null); // можно поставить Map.of("ok", true) + // payload сам станет {} через JsonInboundProcessor log.info("✅ Пользователь добавлен: login={}, loginId={}", req.getLogin(), req.getLoginId()); return resp; diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/utils/NetExceptionResponseFactory.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/utils/NetExceptionResponseFactory.java index 56985a7..35f243e 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/utils/NetExceptionResponseFactory.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/utils/NetExceptionResponseFactory.java @@ -1,12 +1,8 @@ package server.logic.ws_protocol.JSON.utils; - - import server.logic.ws_protocol.JSON.entyties.NetExceptionResponse; import server.logic.ws_protocol.JSON.entyties.NetRequest; -import java.util.Map; - /** * Фабрика ошибок для JSON-протокола. * Создаёт единообразные NetExceptionResponse. @@ -26,11 +22,27 @@ public final class NetExceptionResponseFactory { resp.setOp(req.getOp()); resp.setRequestId(req.getRequestId()); resp.setStatus(status); - resp.setPayload(Map.of( - "code", code, - "message", message - )); + resp.setCode(code); + resp.setMessage(message); + return resp; + } + /** + * Вариант для случаев, когда NetRequest ещё не распарсен, + * но мы уже знаем op и requestId (или они null). + */ + public static NetExceptionResponse error(String op, + String requestId, + int status, + String code, + String message) { + + NetExceptionResponse resp = new NetExceptionResponse(); + resp.setOp(op); + resp.setRequestId(requestId); + resp.setStatus(status); + resp.setCode(code); + resp.setMessage(message); return resp; } } diff --git a/src/main/java/TestJsonWsClient2.java b/src/main/java/Test/TestJsonWsClient2.java similarity index 99% rename from src/main/java/TestJsonWsClient2.java rename to src/main/java/Test/TestJsonWsClient2.java index 2dedb98..1fcd3a5 100644 --- a/src/main/java/TestJsonWsClient2.java +++ b/src/main/java/Test/TestJsonWsClient2.java @@ -1,8 +1,9 @@ +package Test; + import java.net.URI; import java.net.http.HttpClient; import java.net.http.WebSocket; import java.net.http.WebSocket.Listener; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.CountDownLatch; diff --git a/src/main/java/Test/Test_AddUser_FirstAuth.java b/src/main/java/Test/Test_AddUser_FirstAuth.java new file mode 100644 index 0000000..d45403a --- /dev/null +++ b/src/main/java/Test/Test_AddUser_FirstAuth.java @@ -0,0 +1,236 @@ +package Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import utils.crypto.Ed25519Util; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.net.http.WebSocket.Listener; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; + +public class Test_AddUser_FirstAuth { + + // Адрес сервера + private static final String WS_URI = "ws://localhost:7070/ws"; + + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + // Тестовые данные пользователя + private static final String TEST_LOGIN = "anya2"; + private static final long TEST_LOGIN_ID = 100212L; + private static final long TEST_BCH_ID = 4222L; + private static final int TEST_BCH_LIMIT = 1_000_000; + + // Тестовая пара ключей Ed25519 (стабильная, чтобы поведение не прыгало) + private static final byte[] TEST_PRIV_KEY; + private static final String TEST_PUBKEY_B64; + + static { + // Можно сделать детерминированно от логина, чтобы всегда были одинаковые ключи + TEST_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-" + TEST_LOGIN); + byte[] pub = Ed25519Util.derivePublicKey(TEST_PRIV_KEY); + TEST_PUBKEY_B64 = Ed25519Util.keyToBase64(pub); + } + + public static void main(String[] args) throws Exception { + System.out.println("Подключаемся к " + WS_URI); + + CountDownLatch latch = new CountDownLatch(1); + + HttpClient client = HttpClient.newHttpClient(); + + ClientListener listener = new ClientListener(latch); + + client.newWebSocketBuilder() + .buildAsync(URI.create(WS_URI), listener) + .join(); + + // Ждём, пока всё не завершится (успех/ошибка/закрытие) + latch.await(); + System.out.println("Тест завершён, выходим."); + } + + // --- вспомогательные билдера JSON-запросов --- + + // 1) Добавление пользователя + private static String buildAddUserJson() { + return """ + { + "op": "AddUser", + "requestId": "test-add-1", + "login": "%s", + "loginId": %d, + "bchId": %d, + "pubkey0": "%s", + "pubkey1": "%s", + "bchLimit": %d + } + """.formatted( + TEST_LOGIN, + TEST_LOGIN_ID, + TEST_BCH_ID, + TEST_PUBKEY_B64, // pubkey0 + TEST_PUBKEY_B64, // pubkey1 (для теста можно тот же) + TEST_BCH_LIMIT + ); + } + + // 2) Шаг 1 авторизации: запрос sessionPwd + private static String buildAuthStep1Json() { + return """ + { + "op": "AuthSessionNewStep1", + "requestId": "test-auth-1", + "login": "%s" + } + """.formatted(TEST_LOGIN); + } + + // 3) Шаг 2 авторизации: подтверждение подписью + private static String buildAuthStep2Json(String sessionPwd) { + if (sessionPwd == null) { + sessionPwd = ""; + } + + long timeMs = System.currentTimeMillis(); + + // preimage = loginId + timeMs + sessionPwd + String preimageStr = String.valueOf(TEST_LOGIN_ID) + timeMs + sessionPwd; + byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); + + // Подписываем приватным ключом + byte[] sig = Ed25519Util.sign(preimage, TEST_PRIV_KEY); + String sigB64 = Base64.getEncoder().encodeToString(sig); + + return """ + { + "op": "AuthSessionNewStep2", + "requestId": "test-auth-2", + "loginId": %d, + "sigNum": 0, + "timeMs": %d, + "signatureB64": "%s" + } + """.formatted( + TEST_LOGIN_ID, + timeMs, + sigB64 + ); + } + + // ================== LISTENER ================== + + // Внутренний Listener, который сам по шагам шлёт запросы и печатает ответы + private static class ClientListener implements Listener { + + private final CountDownLatch latch; + private int step = 0; // 0 - AddUser, 1 - AuthStep1, 2 - AuthStep2 + private String sessionPwdFromStep1; + + ClientListener(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void onOpen(WebSocket webSocket) { + System.out.println("✅ WebSocket подключен"); + + // Разрешаем приём первого сообщения + webSocket.request(1); + + sendNextRequest(webSocket); + Listener.super.onOpen(webSocket); + } + + // Отправка следующего запроса в зависимости от шага + private void sendNextRequest(WebSocket webSocket) { + switch (step) { + case 0 -> { + String json = buildAddUserJson(); + System.out.println(); + System.out.println("📤 [Шаг 1] Отправляем AddUser:"); + System.out.println(json); + webSocket.sendText(json, true); + } + case 1 -> { + String json = buildAuthStep1Json(); + System.out.println(); + System.out.println("📤 [Шаг 2] Отправляем AuthSessionNewStep1:"); + System.out.println(json); + webSocket.sendText(json, true); + } + case 2 -> { + String json = buildAuthStep2Json(sessionPwdFromStep1); + System.out.println(); + System.out.println("📤 [Шаг 3] Отправляем AuthSessionNewStep2 (подпись):"); + System.out.println(json); + webSocket.sendText(json, true); + } + default -> { + System.out.println("✅ Все шаги выполнены, закрываем соединение"); + webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "all tests done"); + } + } + } + + @Override + public CompletionStage onText(WebSocket webSocket, + CharSequence data, + boolean last) { + String message = data.toString(); + System.out.println("📥 Ответ на шаг " + (step + 1) + ":"); + System.out.println(message); + System.out.println("-----------------------------------------------------"); + + // Если это ответ на шаг 2 (AuthSessionNewStep1) — достаем sessionPwd из payload + if (step == 1) { + sessionPwdFromStep1 = extractSessionPwd(message); + System.out.println("🔑 Извлечён sessionPwd: " + sessionPwdFromStep1); + } + + // Переходим к следующему шагу + step++; + sendNextRequest(webSocket); + + // Запрашиваем следующее входящее сообщение + webSocket.request(1); + + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage()); + error.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public CompletionStage onClose(WebSocket webSocket, + int statusCode, + String reason) { + System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason); + latch.countDown(); + return CompletableFuture.completedFuture(null); + } + + private String extractSessionPwd(String json) { + try { + JsonNode root = JSON_MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("sessionPwd")) { + return payload.get("sessionPwd").asText(); + } + } catch (Exception e) { + System.out.println("⚠️ Не удалось распарсить sessionPwd из ответа: " + e.getMessage()); + } + return null; + } + } +} diff --git a/src/main/java/Test/Test_SessionRefreshClient.java b/src/main/java/Test/Test_SessionRefreshClient.java new file mode 100644 index 0000000..9c63238 --- /dev/null +++ b/src/main/java/Test/Test_SessionRefreshClient.java @@ -0,0 +1,109 @@ +package Test; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.net.http.WebSocket.Listener; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; + +public class Test_SessionRefreshClient { + + // Адрес сервера + private static final String WS_URI = "ws://localhost:7070/ws"; + + // ==== ЗДЕСЬ ПОДСТАВИШЬ СВОИ ДАННЫЕ СЕССИИ ==== + private static final long SESSION_ID = 7599553208996461137L; // TODO: подставь реальный sessionId + private static final String SESSION_PWD = "11b3508f37ae7b41816f42031b90"; // TODO: подставь реальный sessionPwd + // ============================================= + + public static void main(String[] args) throws Exception { + System.out.println("Подключаемся к " + WS_URI); + + CountDownLatch latch = new CountDownLatch(1); + + HttpClient client = HttpClient.newHttpClient(); + + ClientListener listener = new ClientListener(latch); + + client.newWebSocketBuilder() + .buildAsync(URI.create(WS_URI), listener) + .join(); + + latch.await(); + System.out.println("Тест SessionRefresh завершён, выходим."); + } + + private static String buildSessionRefreshJson() { + return """ + { + "op": "SessionRefresh", + "requestId": "test-session-refresh-1", + "sessionId": %d, + "sessionPwd": "%s" + } + """.formatted(SESSION_ID, SESSION_PWD); + } + + private static class ClientListener implements Listener { + + private final CountDownLatch latch; + private boolean sent = false; + + ClientListener(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void onOpen(WebSocket webSocket) { + System.out.println("✅ WebSocket подключен"); + + webSocket.request(1); // разрешаем принимать одно сообщение + + // сразу отправляем запрос SessionRefresh + String json = buildSessionRefreshJson(); + System.out.println(); + System.out.println("📤 Отправляем SessionRefresh:"); + System.out.println(json); + webSocket.sendText(json, true); + sent = true; + + Listener.super.onOpen(webSocket); + } + + @Override + public CompletionStage onText(WebSocket webSocket, + CharSequence data, + boolean last) { + System.out.println("📥 Ответ от сервера:"); + System.out.println(data.toString()); + System.out.println("-----------------------------------------------------"); + + // После одного ответа просто закрываем соединение + System.out.println("✅ Получен ответ на SessionRefresh, закрываем соединение"); + webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "session refresh test done"); + + // запрашиваем следующее сообщение на всякий случай (хотя уже закрываемся) + webSocket.request(1); + + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage()); + error.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public CompletionStage onClose(WebSocket webSocket, + int statusCode, + String reason) { + System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason); + latch.countDown(); + return CompletableFuture.completedFuture(null); + } + } +} diff --git a/src/main/java/TestJsonWsClient.java b/src/main/java/TestJsonWsClient.java deleted file mode 100644 index e273917..0000000 --- a/src/main/java/TestJsonWsClient.java +++ /dev/null @@ -1,135 +0,0 @@ -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.net.http.WebSocket.Listener; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; - -public class TestJsonWsClient { - - // Адрес сервера - private static final String WS_URI = "ws://localhost:7070/ws"; - - // Отдельные запросы - private static final String JSON_REQUEST_SESSION_REFRESH = """ - { - "op": "SessionRefresh", - "requestId": "test-1", - "sessionId": 123, - "sessionPwd": "test-password" - } - """; - - private static final String JSON_REQUEST_ADD_USER = """ - { - "op": "AddUser", - "requestId": "test-add-1", - "login": "anya1111", - "loginId": 100211, - "bchId": 4222, - "pubkey0": "PUB0", - "pubkey1": "PUB1", - "bchLimit": 1000000 - } - """; - - private static final String JSON_REQUEST_AUTH_SESSION_NEW_STEP1 = """ - { - "op": "AuthSessionNewStep1", - "requestId": "test-auth-1", - "login": "anya1111" - } - """; - - // МАССИВ КОНСТАНТА с запросами — добавляешь сюда любые свои JSON - private static final String[] JSON_REQUESTS = { - JSON_REQUEST_SESSION_REFRESH, - JSON_REQUEST_ADD_USER, - JSON_REQUEST_AUTH_SESSION_NEW_STEP1 - }; - - public static void main(String[] args) throws Exception { - System.out.println("Подключаемся к " + WS_URI); - - CountDownLatch latch = new CountDownLatch(1); - - HttpClient client = HttpClient.newHttpClient(); - - ClientListener listener = new ClientListener(JSON_REQUESTS, latch); - - client.newWebSocketBuilder() - .buildAsync(URI.create(WS_URI), listener) - .join(); - - // Ждём, пока всё не завершится (успех/ошибка/закрытие) - latch.await(); - System.out.println("Тест завершён, выходим."); - } - - // Внутренний Listener, который сам по очереди шлёт запросы и печатает ответы - private static class ClientListener implements Listener { - - private final String[] requests; - private final CountDownLatch latch; - private int index = 0; // какой запрос сейчас отправляем/ждём ответ - - ClientListener(String[] requests, CountDownLatch latch) { - this.requests = requests; - this.latch = latch; - } - - @Override - public void onOpen(WebSocket webSocket) { - System.out.println("✅ WebSocket подключен"); - sendNextRequest(webSocket); - Listener.super.onOpen(webSocket); - } - - // Отправка следующего запроса из массива - private void sendNextRequest(WebSocket webSocket) { - if (index < requests.length) { - String json = requests[index]; - System.out.println(); - System.out.println("📤 Отправляем запрос " + (index + 1) + " из " + requests.length + ":"); - System.out.println(json); - webSocket.sendText(json, true); - } else { - System.out.println("✅ Все запросы отправлены, закрываем соединение"); - webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "all tests done"); - } - } - - @Override - public CompletionStage onText(WebSocket webSocket, - CharSequence data, - boolean last) { - // Ответ на текущий запрос (с индексом index) - System.out.println("📥 Ответ на запрос " + (index + 1) + ":"); - System.out.println(data.toString()); - System.out.println("-----------------------------------------------------"); - - // Переходим к следующему запросу - index++; - sendNextRequest(webSocket); - - return CompletableFuture.completedFuture(null); - } - - @Override - public void onError(WebSocket webSocket, Throwable error) { - System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage()); - error.printStackTrace(System.out); - latch.countDown(); - } - - @Override - public CompletionStage onClose(WebSocket webSocket, - int statusCode, - String reason) { - System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason); - latch.countDown(); - return CompletableFuture.completedFuture(null); - } - } -}