This commit is contained in:
AidarKC 2025-12-05 10:56:34 +03:00
parent 5d8dd86c96
commit fc748a744c
23 changed files with 1069 additions and 307 deletions

View File

@ -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') // модуль для работы с БД содержит и сущности из БД и саму работу с БД

View File

@ -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'

View File

@ -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"

View File

@ -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;

View File

@ -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
}

View File

@ -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<String> 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<String> 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])\\..*");
}
}

View File

@ -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);
}
}

View File

@ -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') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
}

View File

@ -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 +
'}';
}

View File

@ -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<String, JsonMessageHandler> HANDLERS = Map.of(
"SessionRefresh", new NetSessionRefreshHandler(),
"AddUser", new NetAddUserHandler(),
"AuthSessionNewStep1", new NetAuthSessionNewStep1Handler()
"AuthSessionNewStep1", new NetAuthSessionNewStep1Handler(),
"AuthSessionNewStep2", new NetAuthSessionNewStep2Handler()
// сюда потом добавишь другие операции
);
private static final Map<String, Class<? extends NetRequest>> REQUEST_TYPES = Map.of(
"SessionRefresh", NetSessionRefreshRequest.class,
"AddUser", NetAddUserRequest.class,
"AuthSessionNewStep1", NetAuthSessionNewStep1Request.class
"AuthSessionNewStep1", NetAuthSessionNewStep1Request.class,
"AuthSessionNewStep2", NetAuthSessionNewStep2Request.class
);
private JsonHandlerRegistry() {

View File

@ -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<String, JsonMessageHandler> JSON_HANDLERS =
JsonHandlerRegistry.getHandlers();
/**
* op класс запроса.
*/
private static final Map<String, Class<? extends NetRequest>> 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<? extends NetRequest> 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 : "";
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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 за разумное число попыток");
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}