Промежуточная версия
This commit is contained in:
AidarKC
2025-12-17 13:06:08 +03:00
parent ab44cc5282
commit eaf1affb27
30 changed files with 926 additions and 563 deletions
@@ -1,8 +1,8 @@
package server.logic.ws_protocol.JSON;
import org.eclipse.jetty.websocket.api.Session;
import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUserEntry;
import shine.db.entities.ActiveSessionEntry;
/**
* ConnectionContext — контекст состояния одного WebSocket-соединения.
@@ -16,10 +16,10 @@ public class ConnectionContext {
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
// Полный пользователь из БД (solana_users)
private SolanaUser solanaUser;
private SolanaUserEntry solanaUserEntry;
// Активная сессия из БД (active_sessions)
private ActiveSession activeSession;
private ActiveSessionEntry activeSessionEntry;
/**
* Идентификатор сессии — base64-строка от 32 байт.
@@ -61,30 +61,30 @@ public class ConnectionContext {
// --- SolanaUser / ActiveSession ---
public SolanaUser getSolanaUser() {
return solanaUser;
public SolanaUserEntry getSolanaUser() {
return solanaUserEntry;
}
public void setSolanaUser(SolanaUser solanaUser) {
this.solanaUser = solanaUser;
public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
this.solanaUserEntry = solanaUserEntry;
}
public ActiveSession getActiveSession() {
return activeSession;
public ActiveSessionEntry getActiveSession() {
return activeSessionEntry;
}
public void setActiveSession(ActiveSession activeSession) {
this.activeSession = activeSession;
public void setActiveSession(ActiveSessionEntry activeSessionEntry) {
this.activeSessionEntry = activeSessionEntry;
}
// --- Удобные геттеры для логина ---
public String getLogin() {
return solanaUser != null ? solanaUser.getLogin() : null;
return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
}
public Long getLoginId() {
return solanaUser != null ? solanaUser.getLoginId() : null;
return solanaUserEntry != null ? solanaUserEntry.getLoginId() : null;
}
// --- sessionId / sessionPwd ---
@@ -134,8 +134,8 @@ public class ConnectionContext {
}
public void reset() {
solanaUser = null;
activeSession = null;
solanaUserEntry = null;
activeSessionEntry = null;
sessionId = null;
sessionPwd = null;
@@ -0,0 +1,38 @@
package server.logic.ws_protocol.JSON.entyties.Blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_AddBlock_new_Request extends Net_Request {
private long blockchainId;
private int globalNumber;
private String prevGlobalHash; // HEX(64) or ""
private int lineNumber; // 0..7
private int lineBlockNumber;
private String prevLineHash; // HEX(64) or ""
private String blockBase64; // base64url of raw .bch bytes
public long getBlockchainId() { return blockchainId; }
public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; }
public int getGlobalNumber() { return globalNumber; }
public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; }
public String getPrevGlobalHash() { return prevGlobalHash; }
public void setPrevGlobalHash(String prevGlobalHash) { this.prevGlobalHash = prevGlobalHash; }
public int getLineNumber() { return lineNumber; }
public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; }
public int getLineBlockNumber() { return lineBlockNumber; }
public void setLineBlockNumber(int lineBlockNumber) { this.lineBlockNumber = lineBlockNumber; }
public String getPrevLineHash() { return prevLineHash; }
public void setPrevLineHash(String prevLineHash) { this.prevLineHash = prevLineHash; }
public String getBlockBase64() { return blockBase64; }
public void setBlockBase64(String blockBase64) { this.blockBase64 = blockBase64; }
}
@@ -0,0 +1,29 @@
package server.logic.ws_protocol.JSON.entyties.Blockchain;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public class Net_AddBlock_new_Response extends Net_Response {
private int serverLastGlobalNumber;
private String serverLastGlobalHash;
private int serverLastLineNumber;
private String serverLastLineHash;
private String reasonCode; // "OUT_OF_SEQUENCE", "HASH_MISMATCH", ...
public int getServerLastGlobalNumber() { return serverLastGlobalNumber; }
public void setServerLastGlobalNumber(int v) { this.serverLastGlobalNumber = v; }
public String getServerLastGlobalHash() { return serverLastGlobalHash; }
public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; }
public int getServerLastLineNumber() { return serverLastLineNumber; }
public void setServerLastLineNumber(int v) { this.serverLastLineNumber = v; }
public String getServerLastLineHash() { return serverLastLineHash; }
public void setServerLastLineHash(String v) { this.serverLastLineHash = v; }
public String getReasonCode() { return reasonCode; }
public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
}
@@ -8,7 +8,7 @@ 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.SolanaUsersDAO;
import shine.db.entities.SolanaUser;
import shine.db.entities.SolanaUserEntry;
import java.security.SecureRandom;
import java.util.Base64;
@@ -49,9 +49,9 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
}
// 2) Ищем пользователя в локальной БД
SolanaUser solanaUser = SolanaUsersDAO.getInstance().getByLogin(login);
SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
if (solanaUser == null) {
if (solanaUserEntry == null) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.UNVERIFIED,
@@ -61,7 +61,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
}
// 3) Заполняем контекст пользователем
ctx.setSolanaUser(solanaUser);
ctx.setSolanaUser(solanaUserEntry);
// 3.1) Отмечаем, что по этому соединению начата авторификация
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
@@ -13,8 +13,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException;
@@ -62,7 +62,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
);
}
SolanaUser user = ctx.getSolanaUser();
SolanaUserEntry user = ctx.getSolanaUser();
long currentLoginId = user.getLoginId();
int authStatus = ctx.getAuthenticationStatus();
@@ -158,7 +158,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
}
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
ActiveSession targetSession;
ActiveSessionEntry targetSession;
try {
targetSession = sessionsDao.getBySessionId(targetSessionId);
} catch (SQLException e) {
@@ -14,8 +14,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSession;
import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import shine.geo.ClientInfoService;
import shine.geo.GeoLookupService;
import utils.crypto.Ed25519Util;
@@ -72,7 +72,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
* @throws IllegalArgumentException при некорректном base64 ключа/подписи
*/
public static boolean verifyAuthorificatedSignature(
SolanaUser user,
SolanaUserEntry user,
String authNonce,
long timeMs,
String signatureB64
@@ -108,7 +108,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
return err;
}
SolanaUser user = ctx.getSolanaUser();
SolanaUserEntry user = ctx.getSolanaUser();
Long loginId = user.getLoginId();
if (loginId == null) {
Net_Response err = NetExceptionResponseFactory.error(
@@ -237,10 +237,10 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
// --- создаём запись ActiveSession и сохраняем в БД ---
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
ActiveSession activeSession;
ActiveSessionEntry activeSessionEntry;
try {
activeSession = new ActiveSession(
activeSessionEntry = new ActiveSessionEntry(
sessionId,
loginId,
newSessionPwd, // настоящий секрет сессии
@@ -256,7 +256,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
userLanguage
);
dao.insert(activeSession);
dao.insert(activeSessionEntry);
} catch (SQLException e) {
log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e);
Net_Response err = NetExceptionResponseFactory.error(
@@ -270,7 +270,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
}
// --- обновляем контекст ---
ctx.setActiveSession(activeSession);
ctx.setActiveSession(activeSessionEntry);
ctx.setSessionId(sessionId);
ctx.setSessionPwd(newSessionPwd); // теперь в контексте хранится секрет сессии
ctx.setAuthNonce(null); // одноразовый nonce больше не нужен
@@ -12,8 +12,8 @@ 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 shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import shine.geo.GeoLookupService;
import java.sql.SQLException;
@@ -50,7 +50,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
);
}
SolanaUser user = ctx.getSolanaUser();
SolanaUserEntry user = ctx.getSolanaUser();
long currentLoginId = user.getLoginId();
int authStatus = ctx.getAuthenticationStatus();
@@ -128,7 +128,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
}
// 3) Тянем все активные сессии пользователя из БД
List<ActiveSession> sessions;
List<ActiveSessionEntry> sessions;
try {
sessions = ActiveSessionsDAO.getInstance().getByLoginId(currentLoginId);
} catch (SQLException e) {
@@ -143,7 +143,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
// 4) Собираем DTO с геолокацией
List<SessionInfo> resultList = new ArrayList<>();
for (ActiveSession s : sessions) {
for (ActiveSessionEntry s : sessions) {
SessionInfo info = new SessionInfo();
info.setSessionId(s.getSessionId());
info.setClientInfoFromClient(s.getClientInfoFromClient());
@@ -13,8 +13,8 @@ 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 shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import shine.geo.ClientInfoService;
import shine.geo.GeoLookupService;
@@ -63,7 +63,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
}
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
ActiveSession session;
ActiveSessionEntry session;
try {
session = sessionsDao.getBySessionId(sessionId);
} catch (SQLException e) {
@@ -96,11 +96,11 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
}
// --- вытаскиваем пользователя по loginId ---
SolanaUser solanaUser = null;
SolanaUserEntry solanaUserEntry = null;
long loginId = session.getLoginId();
try {
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
solanaUser = usersDao.getByLoginId(loginId);
solanaUserEntry = usersDao.getByLoginId(loginId);
} catch (SQLException e) {
log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e);
return NetExceptionResponseFactory.error(
@@ -111,7 +111,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
);
}
if (solanaUser == null) {
if (solanaUserEntry == null) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.UNVERIFIED,
@@ -171,7 +171,7 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
// --- обновляем контекст соединения ---
if (ctx != null) {
ctx.setActiveSession(session);
ctx.setSolanaUser(solanaUser);
ctx.setSolanaUser(solanaUserEntry);
ctx.setSessionId(sessionId);
ctx.setSessionPwd(sessionPwd);
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
@@ -0,0 +1,190 @@
package server.logic.ws_protocol.JSON.handlers.blockchain;
import blockchain.BchBlockEntry;
import blockchain.BodyRecordParser;
import blockchain.body.BodyRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Request;
import server.logic.ws_protocol.JSON.entyties.Blockchain.Net_AddBlock_new_Response;
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.BlockchainStateDAO;
import shine.db.entities.BlockchainStateEntry;
import utils.crypto.BchCryptoVerifier;
import utils.files.FileStoreUtil;
import java.util.Base64;
public class Net_AddBlock_new_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_new_Handler.class);
@Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_AddBlock_new_Request req = (Net_AddBlock_new_Request) baseReq;
// 0) базовые проверки
if (req.getBlockchainId() <= 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCKCHAIN_ID", "blockchainId <= 0");
}
if (req.getGlobalNumber() < 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_GLOBAL_NUMBER", "globalNumber < 0");
}
if (req.getLineNumber() < 0 || req.getLineNumber() > 7) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_NUMBER", "lineNumber must be 0..7");
}
if (req.getLineBlockNumber() < 0) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_LINE_BLOCK_NUMBER", "lineBlockNumber < 0");
}
if (req.getBlockBase64() == null || req.getBlockBase64().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_BLOCK", "blockBase64 is empty");
}
// 1) грузим состояние из БД
BlockchainStateDAO dao = BlockchainStateDAO.getInstance();
BlockchainStateEntry state = dao.getByBlockchainId(req.getBlockchainId());
if (state == null) {
// на MVP можно: запретить добавление, пока цепочка не создана отдельно
// либо разрешить только genesis/header — как ты делал раньше
return NetExceptionResponseFactory.error(req, WireCodes.Status.CHAIN_NOT_FOUND, "CHAIN_NOT_FOUND", "chain not found in DB");
}
// 2) быстрые проверки на “подходит ли блок”
int expectedGlobal = state.getLastGlobalNumber() + 1;
int expectedLine = state.getLastLineNumber(req.getLineNumber()) + 1;
String dbPrevGlobalHash = nn(state.getLastGlobalHash());
String dbPrevLineHash = nn(state.getLastLineHash(req.getLineNumber()));
if (req.getGlobalNumber() != expectedGlobal) {
return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_GLOBAL");
}
if (!eqHash(req.getPrevGlobalHash(), dbPrevGlobalHash)) {
return outOfSeq(req, state, req.getLineNumber(), "GLOBAL_HASH_MISMATCH");
}
if (req.getLineBlockNumber() != expectedLine) {
return outOfSeq(req, state, req.getLineNumber(), "OUT_OF_SEQUENCE_LINE");
}
if (!eqHash(req.getPrevLineHash(), dbPrevLineHash)) {
return outOfSeq(req, state, req.getLineNumber(), "LINE_HASH_MISMATCH");
}
// 3) декодируем блок
byte[] fullBlockBytes;
try {
fullBlockBytes = Base64.getUrlDecoder().decode(req.getBlockBase64());
} catch (IllegalArgumentException e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "blockBase64 decode failed");
}
// 4) парсим .bch
BchBlockEntry block;
try {
block = new BchBlockEntry(fullBlockBytes);
} catch (Exception e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BLOCK_FORMAT", "cannot parse BchBlockEntry");
}
// 5) ПОЛНАЯ валидация: подпись/хэш/тело
// ⚠️ ниже я оставляю общий вызов verifyAll как у тебя раньше,
// но теперь prevHash берём из БД, а publicKey — из state (или из solana_users).
byte[] prevHashGlobal32 = hexToBytes32(dbPrevGlobalHash);
boolean verified = BchCryptoVerifier.verifyAll(
state.getUserLogin(),
req.getBlockchainId(),
prevHashGlobal32,
block.rawBytes,
block.getSignature64(),
block.getHash32(),
Base64.getDecoder().decode(state.getPublicKeyBase64())
);
if (!verified) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "UNVERIFIED", "signature/hash verification failed");
}
// Проверка тела блока
BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check();
// 6) TODO: извлечь lineNumber/lineBlockNumber/prevLineHash из body (если они реально в теле есть)
// и сверить с req + DB. Сейчас оставляю как “крючок”.
// BlockLineMeta meta = BlockLineMetaExtractor.extract(body);
// if (meta.lineNumber != req.getLineNumber()) ...
// if (meta.lineBlockNumber != req.getLineBlockNumber()) ...
// if (!eqHash(meta.prevLineHashHex, dbPrevLineHash)) ...
// 7) запись в файл (фактическое хранение блоков)
FileStoreUtil.getInstance().addDataToBlockchain(req.getBlockchainId(), fullBlockBytes);
// 8) TODO: обновление состояния в БД (вместо BchInfoManager)
// - state.sizeBytes += fullBlockBytes.length
// - state.lastGlobalNumber = req.globalNumber
// - state.lastGlobalHash = bytesToHex(block.getHash32())
// - state.lineX_last_number/hash обновить по lineNumber
// - state.updatedAtMs = now
// dao.upsert(state);
// 9) ответ OK
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
// можно вернуть “новое” состояние, но на MVP вернём хотя бы серверные last’ы до апдейта/после апдейта
resp.setServerLastGlobalNumber(req.getGlobalNumber());
resp.setServerLastGlobalHash(bytesToHex(block.getHash32()));
resp.setServerLastLineNumber(req.getLineBlockNumber());
resp.setServerLastLineHash(resp.getServerLastGlobalHash());
resp.setReasonCode(null);
return resp;
}
private static Net_AddBlock_new_Response outOfSeq(Net_AddBlock_new_Request req, BlockchainStateEntry state, int line, String reason) {
Net_AddBlock_new_Response resp = new Net_AddBlock_new_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OUT_OF_SEQUENCE); // или свой статус
resp.setReasonCode(reason);
resp.setServerLastGlobalNumber(state.getLastGlobalNumber());
resp.setServerLastGlobalHash(nn(state.getLastGlobalHash()));
resp.setServerLastLineNumber(state.getLastLineNumber(line));
resp.setServerLastLineHash(nn(state.getLastLineHash(line)));
return resp;
}
private static boolean eqHash(String a, String b) {
return nn(a).equalsIgnoreCase(nn(b));
}
private static String nn(String s) { return s == null ? "" : s.trim(); }
private static byte[] hexToBytes32(String hex) {
hex = nn(hex);
if (hex.isEmpty()) return new byte[32];
int len = hex.length();
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
if (out.length == 32) return out;
byte[] full = new byte[32];
int copy = Math.min(out.length, 32);
System.arraycopy(out, out.length - copy, full, 32 - copy, copy);
return full;
}
private static String bytesToHex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte x : b) sb.append(String.format("%02x", x));
return sb.toString();
}
}
@@ -11,7 +11,7 @@ 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.SolanaUsersDAO;
import shine.db.entities.SolanaUser;
import shine.db.entities.SolanaUserEntry;
import java.sql.SQLException;
@@ -61,7 +61,7 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
try {
SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
SolanaUser user = new SolanaUser(
SolanaUserEntry user = new SolanaUserEntry(
req.getLoginId(),
req.getLogin(),
req.getBchId(),