04 12 25
This commit is contained in:
parent
5d8dd86c96
commit
fc748a744c
@ -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') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
|
||||
|
||||
@ -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'
|
||||
|
||||
16
shine-server-crypto/src/concat_to_file.sh
Executable file
16
shine-server-crypto/src/concat_to_file.sh
Executable 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"
|
||||
|
||||
@ -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;
|
||||
|
||||
25
shine-server-geo/build.gradle
Normal file
25
shine-server-geo/build.gradle
Normal 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
|
||||
}
|
||||
137
shine-server-geo/src/main/java/shine.geo/GeoLookupService.java
Normal file
137
shine-server-geo/src/main/java/shine.geo/GeoLookupService.java
Normal 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])\\..*");
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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') // модуль для работы с БД содержит и сущности из БД и саму работу с БД
|
||||
|
||||
}
|
||||
|
||||
@ -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 +
|
||||
'}';
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 : "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 за разумное число попыток");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
236
src/main/java/Test/Test_AddUser_FirstAuth.java
Normal file
236
src/main/java/Test/Test_AddUser_FirstAuth.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/main/java/Test/Test_SessionRefreshClient.java
Normal file
109
src/main/java/Test/Test_SessionRefreshClient.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user