diff --git a/SHiNE-browser-plugin-wallet/popup.js b/SHiNE-browser-plugin-wallet/popup.js
index 5f55508..655f74c 100644
--- a/SHiNE-browser-plugin-wallet/popup.js
+++ b/SHiNE-browser-plugin-wallet/popup.js
@@ -19,7 +19,7 @@ const els = {
sessionLogin: document.querySelector('#session-login'),
sessionId: document.querySelector('#session-id'),
sessionType: document.querySelector('#session-type'),
- deviceKeyShort: document.querySelector('#device-key-short'),
+ clientKeyShort: document.querySelector('#client-key-short'),
resumeBtn: document.querySelector('#resume-btn'),
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
disconnectBtn: document.querySelector('#disconnect-btn'),
@@ -134,7 +134,7 @@ function applyState(nextState) {
els.sessionLogin.textContent = session.login || '—';
els.sessionId.textContent = session.sessionId || '—';
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
- els.deviceKeyShort.textContent = shortKey(walletProfile?.publicKeys?.deviceKeyBase58 || '');
+ els.clientKeyShort.textContent = shortKey(walletProfile?.publicKeys?.clientKeyBase58 || '');
}
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
diff --git a/SHiNE-server/shine-server-blockchain/all_files.txt b/SHiNE-server/shine-server-blockchain/all_files.txt
deleted file mode 100644
index b4b46b2..0000000
--- a/SHiNE-server/shine-server-blockchain/all_files.txt
+++ /dev/null
@@ -1,2884 +0,0 @@
-package blockchain;
-
-import blockchain.body.BodyRecord;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * BchBlockEntry — универсальный блок формата SHiNE (Frame v0).
- *
- * =========================================================================
- * FRAME v0 — ФИКСИРОВАННЫЙ ФОРМАТ БЛОКА (ДОКУМЕНТ ПРОТОКОЛА)
- * =========================================================================
- *
- * Все числа BigEndian.
- *
- * PREIMAGE (входит в blockSize, подписывается):
- * [2] frameCode (uint16) код/версия рамки:
- * - 0x0000 = Frame v0 (текущий)
- * [32] prevHash32 (bytes) SHA-256(preimage) предыдущего блока (цепочка)
- * [4] blockSize (int32) размер preimage (в байтах), ВКЛЮЧАЯ frameCode,
- * НО БЕЗ sigMarker и БЕЗ signature64
- * [4] blockNumber (int32) глобальный номер блока (>=0)
- * [8] timestamp (int64) unix seconds
- * [2] type (uint16) тип сообщения
- * [2] subType (uint16) подтип сообщения
- * [2] version (uint16) версия формата сообщения
- * [N] bodyBytes (bytes) тело сообщения (БЕЗ type/subType/version)
- *
- * TAIL (НЕ входит в blockSize, НЕ подписывается в Frame v0):
- * [2] sigMarker (uint16) маркер подписи:
- * - 0x0100 (256) = далее подпись Ed25519 64 байта
- * [64] signature64 (bytes) Ed25519 signature над hash32
- *
- * hash32 НЕ хранится в блоке.
- * hash32 вычисляется при парсинге:
- * preimage = первые blockSize байт
- * hash32 = SHA-256(preimage)
- *
- * Правила MVP-парсера (Frame v0):
- * - frameCode должен быть строго 0x0000, иначе REJECT.
- * - sigMarker должен быть строго 0x0100, иначе REJECT.
- * - подпись обязана присутствовать всегда (sigMarker+signature64).
- * - НИКАКИХ fallback-веток “если маркер другой, то подписи нет/другой хвост”.
- *
- * Важно по безопасности:
- * - sigMarker в v0 не входит в подписываемые байты → его можно подменить,
- * поэтому единственная безопасная логика: "если не 0x0100 — reject".
- * =========================================================================
- */
-public final class BchBlockEntry {
-
- public static final int SIGNATURE_LEN = 64;
- public static final int HASH_LEN = 32;
-
- public static final int FRAME_CODE_LEN = 2;
- public static final int SIG_MARKER_LEN = 2;
-
- /** Frame v0 */
- public static final int FRAME_CODE_V0 = 0x0000;
-
- /** sigMarker: 256 = 0x0100 */
- public static final int SIG_MARKER_ED25519 = 0x0100;
-
- /**
- * Максимальный допустимый размер блока (fullBytes = preimage + sigMarker + signature),
- * чтобы не уложить сервер по памяти/диску.
- */
- public static final int MAX_BLOCK_FULL_BYTES = 4 * 1024 * 1024;
-
- /**
- * Насколько блок может “обгонять” текущее время (защита от кривых часов/вбросов).
- * Если timestamp больше now + 60 сек — блок считаем неверным.
- */
- public static final long MAX_FUTURE_SECONDS = 60;
-
- /**
- * Размер фиксированной части PREIMAGE (без bodyBytes).
- *
- * PREIMAGE header:
- * frameCode(2) + prevHash32(32) + blockSize(4) + blockNumber(4) + timestamp(8)
- * + type(2) + subType(2) + version(2)
- */
- public static final int PREIMAGE_HEADER_SIZE =
- 2 // frameCode
- + 32 // prevHash32
- + 4 // blockSize
- + 4 // blockNumber
- + 8 // timestamp
- + 2 // type
- + 2 // subType
- + 2; // version
-
- /** Минимальный полный размер блока (без bodyBytes). */
- public static final int MIN_FULL_BYTES =
- PREIMAGE_HEADER_SIZE + SIG_MARKER_LEN + SIGNATURE_LEN;
-
- // --- HEADER (PREIMAGE) ---
- public final int frameCode; // uint16 (v0=0)
- public final byte[] prevHash32; // 32
- public final int blockSize; // preimage size (включая frameCode)
- public final int blockNumber; // >=0
- public final long timestamp;
- public final short type;
- public final short subType;
- public final short version;
-
- // --- BODY (PREIMAGE) ---
- public final byte[] bodyBytes;
-
- /** Распарсенное тело (создаётся сразу при парсинге блока). */
- public final BodyRecord body;
-
- // --- TAIL ---
- public final int sigMarker; // uint16 (v0: 0x0100)
- private final byte[] signature64; // 64
-
- // --- derived ---
- private final byte[] hash32; // 32, computed
- private final byte[] preimage; // blockSize bytes
- private final byte[] fullBytes; // preimage + sigMarker + signature
-
- /* ===================================================================== */
- /* ====================== Конструктор из байт ========================== */
- /* ===================================================================== */
-
- public BchBlockEntry(byte[] fullBytes) {
- Objects.requireNonNull(fullBytes, "fullBytes == null");
-
- if (fullBytes.length < MIN_FULL_BYTES) {
- throw new IllegalArgumentException("Block too short: " + fullBytes.length + " < " + MIN_FULL_BYTES);
- }
- if (fullBytes.length > MAX_BLOCK_FULL_BYTES) {
- throw new IllegalArgumentException("Block too large: " + fullBytes.length + " > " + MAX_BLOCK_FULL_BYTES);
- }
-
- ByteBuffer bb = ByteBuffer.wrap(fullBytes).order(ByteOrder.BIG_ENDIAN);
-
- // [2] frameCode
- this.frameCode = Short.toUnsignedInt(bb.getShort());
- if (this.frameCode != FRAME_CODE_V0) {
- throw new IllegalArgumentException(String.format(
- "Bad frameCode: 0x%04X (expected 0x%04X)", this.frameCode, FRAME_CODE_V0
- ));
- }
-
- // [32] prevHash32
- this.prevHash32 = new byte[32];
- bb.get(this.prevHash32);
-
- // [4] blockSize
- this.blockSize = bb.getInt();
- if (blockSize < PREIMAGE_HEADER_SIZE) {
- throw new IllegalArgumentException("blockSize too small: " + blockSize + " < " + PREIMAGE_HEADER_SIZE);
- }
-
- // fullLen must match exactly: blockSize + sigMarker(2) + signature(64)
- int expectedFullLen = blockSize + SIG_MARKER_LEN + SIGNATURE_LEN;
- if (expectedFullLen != fullBytes.length) {
- throw new IllegalArgumentException("blockSize mismatch: blockSize=" + blockSize
- + " expectedFullLen=" + expectedFullLen
- + " fullLen=" + fullBytes.length);
- }
- if (expectedFullLen > MAX_BLOCK_FULL_BYTES) {
- throw new IllegalArgumentException("Block too large by blockSize: " + expectedFullLen + " > " + MAX_BLOCK_FULL_BYTES);
- }
-
- // [4] blockNumber
- this.blockNumber = bb.getInt();
- if (this.blockNumber < 0) {
- throw new IllegalArgumentException("blockNumber < 0: " + this.blockNumber);
- }
-
- // [8] timestamp
- this.timestamp = bb.getLong();
-
- // запрет “в будущее” больше чем на 1 минуту
- long now = Instant.now().getEpochSecond();
- if (this.timestamp > now + MAX_FUTURE_SECONDS) {
- throw new IllegalArgumentException("timestamp is too far in future: ts=" + this.timestamp
- + " now=" + now + " maxFutureSec=" + MAX_FUTURE_SECONDS);
- }
-
- // [2][2][2] type/subType/version
- this.type = bb.getShort();
- this.subType = bb.getShort();
- this.version = bb.getShort();
-
- // [N] bodyBytes
- int bodyLen = blockSize - PREIMAGE_HEADER_SIZE;
- if (bodyLen < 0) {
- throw new IllegalArgumentException("Invalid body length: " + bodyLen);
- }
- this.bodyBytes = new byte[bodyLen];
- bb.get(this.bodyBytes);
-
- // TAIL: [2] sigMarker
- this.sigMarker = Short.toUnsignedInt(bb.getShort());
- if (this.sigMarker != SIG_MARKER_ED25519) {
- throw new IllegalArgumentException(String.format(
- "Bad sigMarker: 0x%04X (expected 0x%04X)", this.sigMarker, SIG_MARKER_ED25519
- ));
- }
-
- // TAIL: [64] signature64
- this.signature64 = new byte[SIGNATURE_LEN];
- bb.get(this.signature64);
-
- // preimage = первые blockSize байт (включая frameCode)
- this.preimage = Arrays.copyOfRange(fullBytes, 0, blockSize);
-
- // hash32 = sha256(preimage)
- this.hash32 = BchCryptoVerifier.sha256(preimage);
-
- // parse body по header.type/subType/version + ОБЯЗАТЕЛЬНЫЙ check()
- this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes);
-
- this.fullBytes = Arrays.copyOf(fullBytes, fullBytes.length);
-
- if (bb.remaining() != 0) {
- throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
- }
- }
-
- /* ===================================================================== */
- /* ====================== Конструктор сборки ============================ */
- /* ===================================================================== */
-
- public BchBlockEntry(byte[] prevHash32,
- int blockNumber,
- long timestamp,
- short type,
- short subType,
- short version,
- byte[] bodyBytes,
- byte[] signature64) {
-
- Objects.requireNonNull(prevHash32, "prevHash32 == null");
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
- Objects.requireNonNull(signature64, "signature64 == null");
-
- if (prevHash32.length != 32) throw new IllegalArgumentException("prevHash32 != 32");
- if (signature64.length != SIGNATURE_LEN) throw new IllegalArgumentException("signature64 != 64");
-
- if (blockNumber < 0) {
- throw new IllegalArgumentException("blockNumber < 0: " + blockNumber);
- }
-
- // запрет “в будущее” больше чем на 1 минуту
- long now = Instant.now().getEpochSecond();
- if (timestamp > now + MAX_FUTURE_SECONDS) {
- throw new IllegalArgumentException("timestamp is too far in future: ts=" + timestamp
- + " now=" + now + " maxFutureSec=" + MAX_FUTURE_SECONDS);
- }
-
- this.frameCode = FRAME_CODE_V0;
- this.prevHash32 = Arrays.copyOf(prevHash32, 32);
- this.blockNumber = blockNumber;
- this.timestamp = timestamp;
- this.type = type;
- this.subType = subType;
- this.version = version;
- this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length);
-
- // blockSize = размер preimage (включая frameCode)
- this.blockSize = PREIMAGE_HEADER_SIZE + this.bodyBytes.length;
-
- int fullLen = this.blockSize + SIG_MARKER_LEN + SIGNATURE_LEN;
- if (fullLen > MAX_BLOCK_FULL_BYTES) {
- throw new IllegalArgumentException("Block too large: " + fullLen + " > " + MAX_BLOCK_FULL_BYTES);
- }
-
- // parse body по header + ОБЯЗАТЕЛЬНЫЙ check()
- this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes);
-
- // tail marker фиксирован
- this.sigMarker = SIG_MARKER_ED25519;
- this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN);
-
- // build preimage
- ByteBuffer pre = ByteBuffer.allocate(blockSize).order(ByteOrder.BIG_ENDIAN);
- pre.putShort((short) (FRAME_CODE_V0 & 0xFFFF));
- pre.put(this.prevHash32);
- pre.putInt(this.blockSize);
- pre.putInt(this.blockNumber);
- pre.putLong(this.timestamp);
- pre.putShort(this.type);
- pre.putShort(this.subType);
- pre.putShort(this.version);
- pre.put(this.bodyBytes);
-
- this.preimage = pre.array();
- this.hash32 = BchCryptoVerifier.sha256(preimage);
-
- // build fullBytes: preimage + sigMarker + signature64
- ByteBuffer full = ByteBuffer.allocate(fullLen).order(ByteOrder.BIG_ENDIAN);
- full.put(this.preimage);
- full.putShort((short) (SIG_MARKER_ED25519 & 0xFFFF));
- full.put(this.signature64);
- this.fullBytes = full.array();
- }
-
- /* ===================================================================== */
- /* ============================ Getters ================================= */
- /* ===================================================================== */
-
- public byte[] getPreimageBytes() {
- return Arrays.copyOf(preimage, preimage.length);
- }
-
- /** Возвращает подпись Ed25519 (64 байта). */
- public byte[] getSignature64() {
- return Arrays.copyOf(signature64, SIGNATURE_LEN);
- }
-
- /** Возвращает hash32 = SHA-256(preimage). */
- public byte[] getHash32() {
- return Arrays.copyOf(hash32, HASH_LEN);
- }
-
- /** Возвращает полный блок: preimage + sigMarker + signature. */
- public byte[] toBytes() {
- return Arrays.copyOf(fullBytes, fullBytes.length);
- }
-
- @Override
- public String toString() {
- String timeIso;
- try {
- timeIso = Instant.ofEpochSecond(timestamp).toString();
- } catch (Exception e) {
- timeIso = "некорректныйTimestamp";
- }
-
- return "BchBlockEntry{"
- + "FRAME{frameCode=0x" + hex4(frameCode)
- + "}, HDR{"
- + "blockSize=" + blockSize
- + ", blockNumber=" + blockNumber
- + ", timestamp=" + timestamp + " (" + timeIso + ")"
- + ", type=" + (type & 0xFFFF)
- + ", subType=" + (subType & 0xFFFF)
- + ", version=" + (version & 0xFFFF)
- + ", prevHash32(hex)=" + toHex(prevHash32)
- + "}"
- + ", BODY{len=" + (bodyBytes == null ? -1 : bodyBytes.length) + "}"
- + ", TAIL{sigMarker=0x" + hex4(sigMarker) + ", signature64(hex)=" + toHex(signature64) + "}"
- + ", DERIVED{hash32(hex)=" + toHex(hash32) + "}"
- + "}";
- }
-
- private static String hex4(int v) {
- String s = Integer.toHexString(v & 0xFFFF);
- while (s.length() < 4) s = "0" + s;
- return s;
- }
-
- private static String toHex(byte[] bytes) {
- if (bytes == null) return "null";
- char[] HEX = "0123456789abcdef".toCharArray();
- char[] out = new char[bytes.length * 2];
- for (int i = 0; i < bytes.length; i++) {
- int vv = bytes[i] & 0xFF;
- out[i * 2] = HEX[vv >>> 4];
- out[i * 2 + 1] = HEX[vv & 0x0F];
- }
- return new String(out);
- }
-}
-package blockchain;
-
-import utils.crypto.Ed25519Util;
-
-import java.security.MessageDigest;
-import java.util.Objects;
-
-/**
- * Верификатор SHiNE (Frame v0):
- *
- * preimage = первые blockSize байт блока (ВКЛЮЧАЯ frameCode=0x0000),
- * = всё до TAIL (sigMarker+signature).
- *
- * hash32 = SHA-256(preimage)
- * verify = Ed25519.verify(hash32, signature64, pubKey32)
- */
-public final class BchCryptoVerifier {
-
- private BchCryptoVerifier() {}
-
- public static byte[] sha256(byte[] data) {
- Objects.requireNonNull(data, "data == null");
- try {
- MessageDigest d = MessageDigest.getInstance("SHA-256");
- return d.digest(data);
- } catch (Exception e) {
- throw new IllegalStateException("SHA-256 unavailable", e);
- }
- }
-
- public static boolean verifyBlock(BchBlockEntry block, byte[] publicKey32) {
- Objects.requireNonNull(block, "block == null");
- Objects.requireNonNull(publicKey32, "publicKey32 == null");
-
- if (publicKey32.length != 32) throw new IllegalArgumentException("publicKey32 != 32");
-
- byte[] hash32 = block.getHash32();
- byte[] sig64 = block.getSignature64();
-
- return Ed25519Util.verify(hash32, sig64, publicKey32);
- }
-}
-package blockchain.body;
-
-/**
- * BodyHasLine — для типов, которые имеют линейные поля в body.
- *
- * Line-prefix (BigEndian) в НАЧАЛЕ bodyBytes:
- * [4] lineCode код линии (root-идентификатор):
- * - 0 для дефолтной линии/канала "0" (root = HEADER, blockNumber=0)
- * - для канала "X": blockNumber root-блока канала (CREATE_CHANNEL)
- *
- * [4] prevLineBlockGlobalNumber глобальный номер предыдущего блока в этой линии
- * [32] prevLineBlockHash32 hash32 предыдущего блока в этой линии
- *
- * [4] lineSeq порядковый номер сообщения внутри линии (1..N)
- *
- * Важно:
- * - Проверка связности линии (prevLineBlockGlobalNumber ↔ prevLineBlockHash32) и корректности lineSeq
- * выполняется на сервере/в БД при вставке (а не в body.check()).
- */
-public interface BodyHasLine {
-
- int lineCode();
-
- int prevLineBlockGlobalNumber();
-
- byte[] prevLineBlockHash32();
-
- int lineSeq();
-}
-package blockchain.body;
-
-import utils.blockchain.BlockchainNameUtil;
-
-/**
- * BodyHasTarget — дополнительный интерфейс для body, которые "ссылаются" на цель (to-поля).
- *
- * Новое правило:
- * - toLogin НЕ храним в байтах блока.
- * - toLogin всегда вычисляется из toBchName по стандарту login+"-NNN".
- *
- * Все методы могут возвращать null.
- */
-public interface BodyHasTarget {
-
- /** login цели (nullable). Вычисляется из toBchName(). */
- default String toLogin() {
- String bch = toBchName();
- if (bch == null) return null;
- return BlockchainNameUtil.loginFromBlockchainName(bch);
- }
-
- /** blockchainName цели (nullable). */
- String toBchName();
-
- /** globalNumber цели (nullable). */
- Integer toBlockGlobalNumber();
-
- /** hash целевого блока (обычно 32 байта). Может быть null, если ссылки нет. */
- byte[] toBlockHashBytes();
-}
-package blockchain.body;
-
-/**
- * BodyRecord — общий контракт для всех типов body (тела блока).
- *
- * ВАЖНО (новый формат):
- * - type/subType/version НЕ лежат в bodyBytes.
- * - type/subType/version читаются из заголовка блока (BchBlockEntry).
- *
- * Поэтому из интерфейса УБРАНЫ:
- * - type()
- * - subType()
- * - version()
- * - expectedLineIndex()
- */
-public interface BodyRecord {
-
- /** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
- BodyRecord check();
-
- /**
- * Сериализовать тело записи в байты (ровно то, что кладётся в block.bodyBytes).
- * Важно: НЕ включает type/subType/version.
- */
- byte[] toBytes();
-}
-package blockchain.body;
-
-import blockchain.MsgSubType;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * ConnectionBody — type=3, ver=1 (в заголовке блока).
- *
- * subType (в заголовке блока) как MsgSubType:
- * FRIEND=10, UNFRIEND=11
- * CONTACT=20, UNCONTACT=21
- * FOLLOW=30, UNFOLLOW=31
- *
- * bodyBytes (BigEndian), новый формат (toLogin НЕ ХРАНИМ):
- * [4] lineCode
- * [4] prevLineNumber
- * [32] prevLineHash32
- * [4] thisLineNumber
- *
- * [1] toBlockchainNameLen (uint8)
- * [N] toBlockchainName UTF-8
- * [4] toBlockGlobalNumber (int32)
- * [32] toBlockHash32 (raw 32 bytes)
- *
- * toLogin вычисляется автоматически из toBlockchainName:
- * toLogin = BlockchainNameUtil.loginFromBlockchainName(toBlockchainName)
- */
-
-/**
- * =========================================================================
- * ПРАВИЛО TARGET/ROOT ДЛЯ КАНАЛОВ И СВЯЗЕЙ (важно для подписок/друзей/контактов)
- * =========================================================================
- *
- * Термины:
- * - ROOT линии/канала = блок, который "начинает" линию:
- * * для канала "0" root = HEADER (blockNumber=0)
- * * для канала "X" root = CREATE_CHANNEL (blockNumber этого блока)
- *
- * 1) СВЯЗИ МЕЖДУ ПОЛЬЗОВАТЕЛЯМИ (CONNECTION_*):
- * FRIEND / CONTACT -> цель ВСЕГДА HEADER пользователя:
- * toBlockNumber = 0
- * toBlockHash32 = hash32(HEADER цели)
- *
- * 2) ПОДПИСКИ НА КОНТЕНТ (FOLLOW/SUBSCRIBE):
- * FOLLOW пользователя (в целом) -> цель = ROOT дефолтного канала "0" (то есть HEADER):
- * toBlockNumber = 0
- * toBlockHash32 = hash32(HEADER цели)
- *
- * FOLLOW/подписка на конкретный канал пользователя ->
- * цель = ROOT этого канала:
- * - канал "0": toBlockNumber=0, toBlockHash32=hash32(HEADER)
- * - канал "X": toBlockNumber=blockNumber(CREATE_CHANNEL),
- * toBlockHash32=hash32(CREATE_CHANNEL)
- *
- * 3) ЗАПРЕТЫ ВАЛИДАЦИИ (желательно на сервере/в БД):
- * - CONNECTION_FRIEND/CONTACT не могут ссылаться на не-HEADER (toBlockNumber != 0 запрещено).
- * - FOLLOW на канал "X" не может ссылаться на произвольный пост внутри канала:
- * разрешено ТОЛЬКО на ROOT (HEADER или CREATE_CHANNEL).
- *
- * Зачем так:
- * - связи и подписки всегда стабильны и не ломаются при новых постах,
- * - один понятный инвариант: "подписка всегда указывает на root линии".
- * =========================================================================
- */
-
-public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasLine {
-
- public static final short TYPE = 3;
- public static final short VER = 1;
-
- public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
-
- public final short subType; // из header
- public final short version; // из header
-
- // line
- public final int lineCode;
- public final int prevLineNumber;
- public final byte[] prevLineHash32;
- public final int thisLineNumber;
-
- // payload
- public final String toBlockchainName;
- public final int toBlockGlobalNumber;
- public final byte[] toBlockHash32;
-
- public ConnectionBody(short subType, short version, byte[] bodyBytes) {
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
-
- this.subType = subType;
- this.version = version;
-
- if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
- throw new IllegalArgumentException("ConnectionBody version must be 1, got=" + (this.version & 0xFFFF));
- }
- if (!isValidSubType(this.subType)) {
- throw new IllegalArgumentException("Bad connection subType: " + (this.subType & 0xFFFF));
- }
-
- // минимум:
- // lineCode(4) + line(4+32+4) + toBchLen[1]+toBch[1] + global[4] + hash[32]
- if (bodyBytes.length < 4 + (4 + 32 + 4) + 1 + 1 + 4 + 32) {
- throw new IllegalArgumentException("ConnectionBody too short");
- }
-
- ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
-
- this.lineCode = bb.getInt();
-
- this.prevLineNumber = bb.getInt();
-
- this.prevLineHash32 = new byte[32];
- bb.get(this.prevLineHash32);
-
- this.thisLineNumber = bb.getInt();
-
- int bchLen = Byte.toUnsignedInt(bb.get());
- if (bchLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
- if (bb.remaining() < bchLen + 4 + 32) throw new IllegalArgumentException("Connection payload too short");
-
- byte[] bchBytes = new byte[bchLen];
- bb.get(bchBytes);
- this.toBlockchainName = new String(bchBytes, StandardCharsets.UTF_8);
-
- this.toBlockGlobalNumber = bb.getInt();
-
- this.toBlockHash32 = new byte[32];
- bb.get(this.toBlockHash32);
-
- if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
- }
-
- public ConnectionBody(int lineCode,
- int prevLineNumber,
- byte[] prevLineHash32,
- int thisLineNumber,
- short subType,
- String toBlockchainName,
- int toBlockGlobalNumber,
- byte[] toBlockHash32) {
-
- Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
- Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
-
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
- if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
-
- if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
- // Железное правило формата: bchName -> login + "-NNN"
- if (BlockchainNameUtil.loginFromBlockchainName(toBlockchainName) == null) {
- throw new IllegalArgumentException("toBlockchainName must match login+\"-NNN\": " + toBlockchainName);
- }
-
- if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
-
- this.lineCode = lineCode;
-
- this.prevLineNumber = prevLineNumber;
- this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- this.thisLineNumber = thisLineNumber;
-
- this.subType = subType;
- this.version = VER;
-
- this.toBlockchainName = toBlockchainName;
- this.toBlockGlobalNumber = toBlockGlobalNumber;
- this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
- }
-
- private static boolean isValidSubType(short st) {
- int v = st & 0xFFFF;
- return v == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)
- || v == (MsgSubType.CONNECTION_UNFRIEND & 0xFFFF)
- || v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
- || v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
- || v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
- || v == (MsgSubType.CONNECTION_UNFOLLOW & 0xFFFF);
- }
-
- @Override
- public ConnectionBody check() {
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
- if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
-
- // line rule (как было)
- if (prevLineNumber == -1) {
- if (!isAllZero32(prevLineHash32)) throw new IllegalArgumentException("prevLineHash32 must be zero when prevLineNumber=-1");
- if (thisLineNumber != -1) throw new IllegalArgumentException("thisLineNumber must be -1 when prevLineNumber=-1");
- } else {
- if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
- }
-
- if (toBlockchainName == null || toBlockchainName.isBlank())
- throw new IllegalArgumentException("toBlockchainName is blank");
-
- // гарантируем вычислимый toLogin (иначе target “битый” по стандарту)
- if (BlockchainNameUtil.loginFromBlockchainName(toBlockchainName) == null)
- throw new IllegalArgumentException("toBlockchainName must match login+\"-NNN\": " + toBlockchainName);
-
- if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 invalid");
-
- return this;
- }
-
- @Override
- public byte[] toBytes() {
- byte[] bchBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
- if (bchBytes.length == 0 || bchBytes.length > 255)
- throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
-
- if (toBlockHash32 == null || toBlockHash32.length != 32)
- throw new IllegalArgumentException("toBlockHash32 != 32");
-
- int cap = 4 + (4 + 32 + 4)
- + 1 + bchBytes.length
- + 4 + 32;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
-
- bb.putInt(lineCode);
-
- bb.putInt(prevLineNumber);
- bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- bb.putInt(thisLineNumber);
-
- bb.put((byte) bchBytes.length);
- bb.put(bchBytes);
-
- bb.putInt(toBlockGlobalNumber);
- bb.put(toBlockHash32);
-
- return bb.array();
- }
-
- private static boolean isAllZero32(byte[] b) {
- if (b == null || b.length != 32) return true;
- for (int i = 0; i < 32; i++) if (b[i] != 0) return false;
- return true;
- }
-
- /* ====================== BodyHasLine ====================== */
- @Override public int lineCode() { return lineCode; }
- @Override public int prevLineBlockGlobalNumber() { return prevLineNumber; }
- @Override public byte[] prevLineBlockHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
- @Override public int lineSeq() { return thisLineNumber; }
-
- /* ====================== BodyHasTarget ===================== */
- @Override public String toBchName() { return toBlockchainName; }
- @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
- @Override public byte[] toBlockHashBytes() { return toBlockHash32; }
-}
-package blockchain.body;
-
-import blockchain.MsgSubType;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * CreateChannelBody — TECH сообщение создания канала.
- *
- * type=0, ver=1 (в заголовке блока)
- * subType=MsgSubType.TECH_CREATE_CHANNEL (=1)
- *
- * Это сообщение идёт по ТЕХ-ЛИНИИ (hasLine):
- * - prevLineNumber/hash указывают на предыдущее TECH-сообщение (HEADER или прошлый CREATE_CHANNEL)
- * - thisLineNumber: 1,2,3... (тех-нумерация)
- *
- * bodyBytes (BigEndian), новый формат line-prefix:
- * [4] lineCode (для TECH линии обычно 0)
- * [4] prevLineNumber
- * [32] prevLineHash32
- * [4] thisLineNumber
- * [1] channelNameLen (uint8)
- * [N] channelName UTF-8 (^[A-Za-z0-9_]+$)
- *
- * Важно:
- * - канал "0" зарезервирован (создаётся по умолчанию от HEADER), создавать его нельзя.
- */
-public final class CreateChannelBody implements BodyRecord, BodyHasLine {
-
- public static final short TYPE = 0;
- public static final short VER = 1;
-
- public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
-
- public static final short SUBTYPE = MsgSubType.TECH_CREATE_CHANNEL;
-
- private static final byte[] ZERO32 = new byte[32];
-
- public final short subType; // из header
- public final short version; // из header
-
- // line
- public final int lineCode;
- public final int prevLineNumber;
- public final byte[] prevLineHash32; // 32
- public final int thisLineNumber;
-
- // payload
- public final String channelName;
-
- public CreateChannelBody(short subType, short version, byte[] bodyBytes) {
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
-
- this.subType = subType;
- this.version = version;
-
- if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
- throw new IllegalArgumentException("CreateChannelBody version must be 1, got=" + (this.version & 0xFFFF));
- }
- if ((this.subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) {
- throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1), got=" + (this.subType & 0xFFFF));
- }
-
- // минимум: lineCode(4) + line(4+32+4) + nameLen(1) + name(1)
- if (bodyBytes.length < 4 + (4 + 32 + 4) + 1 + 1) {
- throw new IllegalArgumentException("CreateChannelBody too short");
- }
-
- ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
-
- this.lineCode = bb.getInt();
-
- this.prevLineNumber = bb.getInt();
-
- this.prevLineHash32 = new byte[32];
- bb.get(this.prevLineHash32);
-
- this.thisLineNumber = bb.getInt();
-
- int nameLen = Byte.toUnsignedInt(bb.get());
- if (nameLen <= 0) throw new IllegalArgumentException("channelNameLen is 0");
- if (bb.remaining() != nameLen) {
- throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " nameLen=" + nameLen);
- }
-
- byte[] nameBytes = new byte[nameLen];
- bb.get(nameBytes);
-
- this.channelName = new String(nameBytes, StandardCharsets.UTF_8);
-
- if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
- }
-
- public CreateChannelBody(int lineCode,
- int prevLineNumber,
- byte[] prevLineHash32,
- int thisLineNumber,
- String channelName) {
- Objects.requireNonNull(channelName, "channelName == null");
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
-
- this.subType = SUBTYPE;
- this.version = VER;
-
- this.lineCode = lineCode;
- this.prevLineNumber = prevLineNumber;
- this.prevLineHash32 = (prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32));
- this.thisLineNumber = thisLineNumber;
-
- this.channelName = channelName;
- }
-
- @Override
- public CreateChannelBody check() {
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
-
- if ((subType & 0xFFFF) != (SUBTYPE & 0xFFFF))
- throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1)");
-
- if (channelName == null || channelName.isBlank())
- throw new IllegalArgumentException("channelName is blank");
-
- if (!channelName.matches("^[A-Za-z0-9_]+$"))
- throw new IllegalArgumentException("channelName must match ^[A-Za-z0-9_]+$");
-
- if ("0".equals(channelName))
- throw new IllegalArgumentException("channelName \"0\" is reserved");
-
- // tech-line: prev обязателен (минимум HEADER=0)
- if (prevLineNumber < 0)
- throw new IllegalArgumentException("prevLineNumber must be >=0 for CreateChannelBody");
- if (prevLineHash32 == null || prevLineHash32.length != 32)
- throw new IllegalArgumentException("prevLineHash32 invalid");
- if (thisLineNumber <= 0)
- throw new IllegalArgumentException("thisLineNumber must be >=1 for CreateChannelBody");
-
- return this;
- }
-
- @Override
- public byte[] toBytes() {
- byte[] nameUtf8 = channelName.getBytes(StandardCharsets.UTF_8);
- if (nameUtf8.length == 0 || nameUtf8.length > 255)
- throw new IllegalArgumentException("channelName utf8 len must be 1..255");
-
- int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length;
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
-
- bb.putInt(lineCode);
-
- bb.putInt(prevLineNumber);
- bb.put(prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32));
- bb.putInt(thisLineNumber);
-
- bb.put((byte) nameUtf8.length);
- bb.put(nameUtf8);
-
- return bb.array();
- }
-
- /* ====================== BodyHasLine ====================== */
- @Override public int lineCode() { return lineCode; }
- @Override public int prevLineBlockGlobalNumber() { return prevLineNumber; }
- @Override public byte[] prevLineBlockHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
- @Override public int lineSeq() { return thisLineNumber; }
-}
-package blockchain.body;
-
-import utils.config.ShineSignatureConstants;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.util.Objects;
-
-/**
- * HeaderBody — type=0, version=1.
- *
- * В новом формате type/subType/version живут в HEADER блока,
- * поэтому bodyBytes для HeaderBody содержат только payload:
- *
- * bodyBytes (BigEndian):
- * [TAG_LEN] tag ASCII "SHiNE"
- * [1] loginLength=N (uint8)
- * [N] login UTF-8
- */
-public final class HeaderBody implements BodyRecord {
-
- public static final short TYPE = 0;
- public static final short VER = 1;
-
- public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
-
- /** Для header subType всегда 0 (служебная совместимость). */
- public static final short SUBTYPE_COMPAT = 0;
-
- /** TAG формата (ASCII). */
- public static final String TAG = ShineSignatureConstants.BLOCKCHAIN_HEADER_TAG;
-
- private static final byte[] TAG_ASCII = TAG.getBytes(StandardCharsets.US_ASCII);
- private static final int TAG_LEN = TAG_ASCII.length;
-
- public final short subType; // всегда 0 (из заголовка блока)
- public final short version; // из заголовка блока
- public final String tag; // "SHiNE"
- public final String login;
-
- /** Десериализация из payload bodyBytes (без type/subType/version). */
- public HeaderBody(short subType, short version, byte[] bodyBytes) {
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
-
- this.subType = subType;
- this.version = version;
-
- if ((this.subType & 0xFFFF) != (SUBTYPE_COMPAT & 0xFFFF)) {
- throw new IllegalArgumentException("HeaderBody subType must be 0, got=" + (this.subType & 0xFFFF));
- }
- if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
- throw new IllegalArgumentException("HeaderBody version must be 1, got=" + (this.version & 0xFFFF));
- }
-
- // минимум: tag[TAG_LEN] + loginLen[1]
- if (bodyBytes.length < TAG_LEN + 1) throw new IllegalArgumentException("HeaderBody too short");
-
- ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
-
- byte[] tagBytes = new byte[TAG_LEN];
- bb.get(tagBytes);
- String t = new String(tagBytes, StandardCharsets.US_ASCII);
- if (!TAG.equals(t)) throw new IllegalArgumentException("Bad tag: " + t);
- this.tag = t;
-
- int loginLen = Byte.toUnsignedInt(bb.get());
- if (loginLen <= 0 || bb.remaining() < loginLen)
- throw new IllegalArgumentException("Bad login length");
-
- byte[] loginBytes = new byte[loginLen];
- bb.get(loginBytes);
- this.login = new String(loginBytes, StandardCharsets.UTF_8);
-
- if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
- }
-
- /** Создание “вручную”. */
- public HeaderBody(String login) {
- Objects.requireNonNull(login, "login == null");
- this.subType = SUBTYPE_COMPAT;
- this.version = VER;
- this.tag = TAG;
- this.login = login;
- }
-
- @Override
- public HeaderBody check() {
- if ((subType & 0xFFFF) != (SUBTYPE_COMPAT & 0xFFFF))
- throw new IllegalArgumentException("HeaderBody subType must be 0");
-
- if (login == null || login.isBlank())
- throw new IllegalArgumentException("Login is blank");
- if (!login.matches("^[A-Za-z0-9_]+$"))
- throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$");
-
- return this;
- }
-
- @Override
- public byte[] toBytes() {
- byte[] loginUtf8 = login.getBytes(StandardCharsets.UTF_8);
- if (loginUtf8.length == 0 || loginUtf8.length > 255)
- throw new IllegalArgumentException("Login utf8 len must be 1..255");
-
- int cap = TAG_LEN + 1 + loginUtf8.length;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
- bb.put(TAG_ASCII);
- bb.put((byte) loginUtf8.length);
- bb.put(loginUtf8);
-
- return bb.array();
- }
-
- @Override
- public String toString() {
- return """
- HeaderBody {
- тип записи : HEADER (type=0, ver=1) [в заголовке блока]
- subType : 0 (compat)
- тег формата : "%s"
- login владельца : "%s"
- }
- """.formatted(tag, login);
- }
-}
-package blockchain.body;
-
-import blockchain.MsgSubType;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * ReactionBody — type=2, version=1 (в заголовке блока).
- *
- * subType (в заголовке блока):
- * 1 = LIKE
- *
- * bodyBytes (BigEndian), новый формат:
- * [1] toBlockchainNameLen (uint8)
- * [N] toBlockchainName UTF-8
- * [4] toBlockGlobalNumber (int32)
- * [32] toBlockHash32 (raw 32 bytes)
- *
- * ЛИНИИ НЕТ.
- */
-public final class ReactionBody implements BodyRecord, BodyHasTarget {
-
- public static final short TYPE = 2;
- public static final short VER = 1;
-
- public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
-
- public final short subType; // из header
- public final short version; // из header
-
- public final String toBlockchainName;
- public final int toBlockGlobalNumber;
- public final byte[] toBlockHash32;
-
- public ReactionBody(short subType, short version, byte[] bodyBytes) {
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
-
- this.subType = subType;
- this.version = version;
-
- if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
- throw new IllegalArgumentException("ReactionBody version must be 1, got=" + (this.version & 0xFFFF));
- }
- if ((this.subType & 0xFFFF) != (MsgSubType.REACTION_LIKE & 0xFFFF)) {
- throw new IllegalArgumentException("Bad reaction subType: " + (this.subType & 0xFFFF));
- }
-
- // минимум: nameLen[1]+name[1]+global[4]+hash[32]
- if (bodyBytes.length < 1 + 1 + 4 + 32) throw new IllegalArgumentException("ReactionBody too short");
-
- ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
-
- int nameLen = Byte.toUnsignedInt(bb.get());
- if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
- if (bb.remaining() < nameLen + 4 + 32) throw new IllegalArgumentException("ReactionBody payload too short");
-
- byte[] nameBytes = new byte[nameLen];
- bb.get(nameBytes);
- this.toBlockchainName = new String(nameBytes, StandardCharsets.UTF_8);
-
- this.toBlockGlobalNumber = bb.getInt();
-
- this.toBlockHash32 = new byte[32];
- bb.get(this.toBlockHash32);
-
- if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
- }
-
- public ReactionBody(String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32) {
- Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
- Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
-
- this.subType = MsgSubType.REACTION_LIKE;
- this.version = VER;
-
- if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
- if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
-
- this.toBlockchainName = toBlockchainName;
- this.toBlockGlobalNumber = toBlockGlobalNumber;
- this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
- }
-
- @Override
- public ReactionBody check() {
- if ((subType & 0xFFFF) != (MsgSubType.REACTION_LIKE & 0xFFFF))
- throw new IllegalArgumentException("Bad reaction subType: " + (subType & 0xFFFF));
-
- if (toBlockchainName == null || toBlockchainName.isBlank())
- throw new IllegalArgumentException("toBlockchainName is blank");
- if (toBlockGlobalNumber < 0)
- throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32 == null || toBlockHash32.length != 32)
- throw new IllegalArgumentException("toBlockHash32 invalid");
-
- return this;
- }
-
- @Override
- public byte[] toBytes() {
- byte[] nameBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
- if (nameBytes.length == 0 || nameBytes.length > 255)
- throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
-
- int cap = 1 + nameBytes.length + 4 + 32;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
- bb.put((byte) nameBytes.length);
- bb.put(nameBytes);
- bb.putInt(toBlockGlobalNumber);
- bb.put(toBlockHash32);
-
- return bb.array();
- }
-
- /* ====================== BodyHasTarget ====================== */
-
- @Override public String toBchName() { return toBlockchainName; }
- @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
- @Override public byte[] toBlockHashBytes() { return toBlockHash32; }
-}
-package blockchain;
-
-import blockchain.body.*;
-
-/**
- * Парсер body выбирает класс по header: type/subType/version,
- * потому что bodyBytes больше НЕ содержат type/subType/version.
- */
-public final class BodyRecordParser {
-
- private BodyRecordParser() {}
-
- public static BodyRecord parse(short type, short subType, short version, byte[] bodyBytes) {
- if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null");
-
- int t = type & 0xFFFF;
- int v = version & 0xFFFF;
-
- int key = (t << 16) | v;
-
- BodyRecord r = switch (key) {
- case HeaderBody.KEY -> {
- int st = subType & 0xFFFF;
- if (st == (HeaderBody.SUBTYPE_COMPAT & 0xFFFF)) {
- yield new HeaderBody(subType, version, bodyBytes);
- }
- if (st == (CreateChannelBody.SUBTYPE & 0xFFFF)) {
- yield new CreateChannelBody(subType, version, bodyBytes);
- }
- throw new IllegalArgumentException("Unknown TECH subType for type=0 ver=1: subType=" + st);
- }
-
- // TEXT type=1 ver=1: выбираем класс по subType
- case TextBody.KEY -> {
- int st = subType & 0xFFFF;
-
- if (st == (MsgSubType.TEXT_POST & 0xFFFF)
- || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- yield new TextLineBody(subType, version, bodyBytes);
- }
-
- if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)
- || st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
- yield new TextReplyBody(subType, version, bodyBytes);
- }
-
- throw new IllegalArgumentException("Unknown TEXT subType for type=1 ver=1: subType=" + st);
- }
-
- case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
- case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
- case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
-
- default -> throw new IllegalArgumentException(String.format(
- "Unknown body type/version from header: type=%d ver=%d subType=%d",
- t, v, (subType & 0xFFFF)
- ));
- };
-
- return r.check();
- }
-}
-package blockchain.body;
-
-import blockchain.MsgSubType;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.CodingErrorAction;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * TextBody — type=1, ver=1 (в заголовке блока).
- *
- * subType (в заголовке блока):
- * 10 = POST
- * 11 = EDIT_POST
- * 20 = REPLY
- * 21 = EDIT_REPLY
- *
- * =========================================================================
- * КОНЦЕПЦИЯ ЛИНИЙ ДЛЯ ТЕКСТОВЫХ СООБЩЕНИЙ:
- *
- * POST и EDIT_POST принадлежат ЛИНИИ КАНАЛА и имеют hasLine.
- * В новом формате добавлен lineCode:
- * lineCode = 0 для канала "0"
- * lineCode = blockNumber "заглавия линии/канала" (например CREATE_CHANNEL)
- *
- * REPLY и EDIT_REPLY НЕ имеют линии (нет hasLine в байтах).
- *
- * =========================================================================
- * ФОРМАТЫ bodyBytes (BigEndian):
- *
- * 1) POST (subType=10):
- * [4] lineCode
- * [4] prevLineNumber
- * [32] prevLineHash32
- * [4] thisLineNumber
- * [2] textLenBytes (uint16)
- * [N] text UTF-8
- *
- * 2) EDIT_POST (subType=11):
- * [4] lineCode
- * [4] prevLineNumber
- * [32] prevLineHash32
- * [4] thisLineNumber
- *
- * hasTarget (на ОРИГИНАЛЬНЫЙ POST, toBchName НЕ хранить):
- * [4] toBlockGlobalNumber
- * [32] toBlockHash32
- *
- * [2] textLenBytes (uint16)
- * [N] text UTF-8
- *
- * 3) REPLY (subType=20) — НЕ в линии:
- * hasTarget:
- * [1] toBlockchainNameLen (uint8)
- * [N] toBlockchainName UTF-8
- * [4] toBlockGlobalNumber
- * [32] toBlockHash32
- *
- * [2] textLenBytes (uint16)
- * [M] text UTF-8
- *
- * 4) EDIT_REPLY (subType=21) — НЕ в линии:
- * hasTarget (на ОРИГИНАЛЬНЫЙ REPLY, toBchName НЕ хранить):
- * [4] toBlockGlobalNumber
- * [32] toBlockHash32
- *
- * [2] textLenBytes (uint16)
- * [N] text UTF-8
- */
-public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
-
- public static final short TYPE = 1;
- public static final short VER = 1;
-
- public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
-
- public final short subType; // из header
- public final short version; // из header
-
- // ===== line fields (только для POST/EDIT_POST) =====
- // Для REPLY/EDIT_REPLY эти поля НЕ сериализуются; значения держим как "пустые".
- public final int lineCode; // только для line-message; иначе -1
- public final int prevLineNumber;
- public final byte[] prevLineHash32; // 32 or null
- public final int thisLineNumber;
-
- // ===== message text =====
- public final String message;
-
- // ===== target fields =====
- // REPLY: toBlockchainName + globalNumber + hash32
- // EDIT_POST / EDIT_REPLY: только globalNumber + hash32 (без toBlockchainName)
- public final String toBlockchainName; // nullable
- public final Integer toBlockGlobalNumber; // nullable
- public final byte[] toBlockHash32; // nullable (но если target есть -> 32)
-
- /* ===================================================================== */
- /* ====================== Конструктор из байт ========================== */
- /* ===================================================================== */
-
- public TextBody(short subType, short version, byte[] bodyBytes) {
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
-
- this.subType = subType;
- this.version = version;
-
- if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
- throw new IllegalArgumentException("TextBody version must be 1, got=" + (this.version & 0xFFFF));
- }
- if (!isValidSubType(this.subType)) {
- throw new IllegalArgumentException("Bad Text subType: " + (this.subType & 0xFFFF));
- }
-
- ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
-
- int st = this.subType & 0xFFFF;
-
- if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
- // POST: hasLine(lineCode+line) + text
- ensureMin(bb, (4 + 4 + 32 + 4) + 2, "POST too short");
-
- this.lineCode = bb.getInt();
- this.prevLineNumber = bb.getInt();
- this.prevLineHash32 = new byte[32];
- bb.get(this.prevLineHash32);
- this.thisLineNumber = bb.getInt();
-
- this.message = readStrictUtf8Len16(bb, "POST text");
-
- this.toBlockchainName = null;
- this.toBlockGlobalNumber = null;
- this.toBlockHash32 = null;
-
- ensureNoTail(bb, "POST");
-
- } else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- // EDIT_POST: hasLine(lineCode+line) + target(no bch) + text
- ensureMin(bb, (4 + 4 + 32 + 4) + (4 + 32) + 2, "EDIT_POST too short");
-
- this.lineCode = bb.getInt();
- this.prevLineNumber = bb.getInt();
- this.prevLineHash32 = new byte[32];
- bb.get(this.prevLineHash32);
- this.thisLineNumber = bb.getInt();
-
- int tgtNum = bb.getInt();
- byte[] tgtHash = new byte[32];
- bb.get(tgtHash);
-
- this.toBlockchainName = null;
- this.toBlockGlobalNumber = tgtNum;
- this.toBlockHash32 = tgtHash;
-
- this.message = readStrictUtf8Len16(bb, "EDIT_POST text");
-
- ensureNoTail(bb, "EDIT_POST");
-
- } else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
- // REPLY: target(with bch) + text (без line)
- ensureMin(bb, 1 + 1 + 4 + 32 + 2, "REPLY too short");
-
- int nameLen = Byte.toUnsignedInt(bb.get());
- if (nameLen <= 0) throw new IllegalArgumentException("REPLY toBlockchainNameLen is 0");
- ensureMin(bb, nameLen + 4 + 32 + 2, "REPLY payload too short");
-
- byte[] nameBytes = new byte[nameLen];
- bb.get(nameBytes);
- this.toBlockchainName = new String(nameBytes, StandardCharsets.UTF_8);
-
- this.toBlockGlobalNumber = bb.getInt();
-
- this.toBlockHash32 = new byte[32];
- bb.get(this.toBlockHash32);
-
- this.message = readStrictUtf8Len16(bb, "REPLY text");
-
- // line fields отсутствуют в байтах
- this.lineCode = -1;
- this.prevLineNumber = -1;
- this.prevLineHash32 = null;
- this.thisLineNumber = -1;
-
- ensureNoTail(bb, "REPLY");
-
- } else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
- // EDIT_REPLY: target(no bch) + text (без line)
- ensureMin(bb, (4 + 32) + 2, "EDIT_REPLY too short");
-
- int tgtNum = bb.getInt();
- byte[] tgtHash = new byte[32];
- bb.get(tgtHash);
-
- this.toBlockchainName = null;
- this.toBlockGlobalNumber = tgtNum;
- this.toBlockHash32 = tgtHash;
-
- this.message = readStrictUtf8Len16(bb, "EDIT_REPLY text");
-
- // line fields отсутствуют в байтах
- this.lineCode = -1;
- this.prevLineNumber = -1;
- this.prevLineHash32 = null;
- this.thisLineNumber = -1;
-
- ensureNoTail(bb, "EDIT_REPLY");
-
- } else {
- throw new IllegalArgumentException("Unsupported Text subType: " + st);
- }
- }
-
- /* ===================================================================== */
- /* ====================== Фабрики (удобно) ============================= */
- /* ===================================================================== */
-
- public static TextBody newPost(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, String message) {
- return new TextBody(MsgSubType.TEXT_POST, lineCode, prevLineNumber, prevLineHash32, thisLineNumber,
- message, null, null, null);
- }
-
- public static TextBody newEditPost(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber,
- int targetBlockNumber, byte[] targetHash32,
- String message) {
- return new TextBody(MsgSubType.TEXT_EDIT_POST, lineCode, prevLineNumber, prevLineHash32, thisLineNumber,
- message, null, targetBlockNumber, targetHash32);
- }
-
- public static TextBody newReply(String toBlockchainName, int targetBlockNumber, byte[] targetHash32, String message) {
- return new TextBody(MsgSubType.TEXT_REPLY, -1, -1, null, -1,
- message, toBlockchainName, targetBlockNumber, targetHash32);
- }
-
- public static TextBody newEditReply(int targetBlockNumber, byte[] targetHash32, String message) {
- return new TextBody(MsgSubType.TEXT_EDIT_REPLY, -1, -1, null, -1,
- message, null, targetBlockNumber, targetHash32);
- }
-
- /**
- * Универсальный конструктор “вручную”.
- * Для REPLY/EDIT_REPLY line поля игнорируются при сериализации (их в формате нет).
- */
- public TextBody(short subType,
- int lineCode,
- int prevLineNumber,
- byte[] prevLineHash32,
- int thisLineNumber,
- String message,
- String toBlockchainName,
- Integer toBlockGlobalNumber,
- byte[] toBlockHash32) {
-
- Objects.requireNonNull(message, "message == null");
-
- if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
- if (message.isBlank()) throw new IllegalArgumentException("message is blank");
-
- this.subType = subType;
- this.version = VER;
-
- int st = subType & 0xFFFF;
-
- // line применима только к POST/EDIT_POST
- if (st == (MsgSubType.TEXT_POST & 0xFFFF) || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0 for line message");
- this.lineCode = lineCode;
- this.prevLineNumber = prevLineNumber;
- this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- this.thisLineNumber = thisLineNumber;
- } else {
- this.lineCode = -1;
- this.prevLineNumber = -1;
- this.prevLineHash32 = null;
- this.thisLineNumber = -1;
- }
-
- this.message = message;
-
- // target правила
- if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
- this.toBlockchainName = null;
- this.toBlockGlobalNumber = null;
- this.toBlockHash32 = null;
-
- } else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
- Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
- if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
-
- this.toBlockchainName = null; // по ТЗ: не хранить
- this.toBlockGlobalNumber = toBlockGlobalNumber;
- this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
-
- } else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
- Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
- Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
- Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
- if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
- if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
-
- this.toBlockchainName = toBlockchainName;
- this.toBlockGlobalNumber = toBlockGlobalNumber;
- this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
-
- } else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
- Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
- Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
- if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
-
- this.toBlockchainName = null; // по ТЗ: не хранить
- this.toBlockGlobalNumber = toBlockGlobalNumber;
- this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
-
- } else {
- this.toBlockchainName = null;
- this.toBlockGlobalNumber = null;
- this.toBlockHash32 = null;
- }
- }
-
- private static boolean isValidSubType(short st) {
- int v = st & 0xFFFF;
- return v == (MsgSubType.TEXT_POST & 0xFFFF)
- || v == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
- || v == (MsgSubType.TEXT_REPLY & 0xFFFF)
- || v == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF);
- }
-
- @Override
- public TextBody check() {
- if (!isValidSubType(subType))
- throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
-
- if (message == null || message.isBlank())
- throw new IllegalArgumentException("Text message is blank");
-
- int st = subType & 0xFFFF;
-
- // локальные проверки line (БД не трогаем)
- if (st == (MsgSubType.TEXT_POST & 0xFFFF) || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0 for line message");
- if (prevLineHash32 == null || prevLineHash32.length != 32)
- throw new IllegalArgumentException("prevLineHash32 invalid");
- } else {
- // reply/edit_reply: line отсутствует
- if (prevLineHash32 != null)
- throw new IllegalArgumentException("REPLY/EDIT_REPLY must not contain line hash");
- }
-
- // target rules
- if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
- if (toBlockchainName != null || toBlockGlobalNumber != null || toBlockHash32 != null)
- throw new IllegalArgumentException("POST must not contain target fields");
-
- } else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- if (toBlockchainName != null)
- throw new IllegalArgumentException("EDIT_POST must not contain toBlockchainName in target");
- if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0)
- throw new IllegalArgumentException("EDIT_POST toBlockGlobalNumber invalid");
- if (toBlockHash32 == null || toBlockHash32.length != 32)
- throw new IllegalArgumentException("EDIT_POST toBlockHash32 invalid");
-
- } else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
- if (toBlockchainName == null || toBlockchainName.isBlank())
- throw new IllegalArgumentException("REPLY toBlockchainName is blank");
- if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0)
- throw new IllegalArgumentException("REPLY toBlockGlobalNumber invalid");
- if (toBlockHash32 == null || toBlockHash32.length != 32)
- throw new IllegalArgumentException("REPLY toBlockHash32 invalid");
-
- } else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
- if (toBlockchainName != null)
- throw new IllegalArgumentException("EDIT_REPLY must not contain toBlockchainName in target");
- if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0)
- throw new IllegalArgumentException("EDIT_REPLY toBlockGlobalNumber invalid");
- if (toBlockHash32 == null || toBlockHash32.length != 32)
- throw new IllegalArgumentException("EDIT_REPLY toBlockHash32 invalid");
- }
-
- return this;
- }
-
- @Override
- public byte[] toBytes() {
- byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
- if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
- if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
-
- int st = subType & 0xFFFF;
-
- if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
- // hasLine(lineCode+line) + text
- int cap = (4 + 4 + 32 + 4) + 2 + msgUtf8.length;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
- bb.putInt(lineCode);
- bb.putInt(prevLineNumber);
- bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- bb.putInt(thisLineNumber);
- bb.putShort((short) msgUtf8.length);
- bb.put(msgUtf8);
- return bb.array();
-
- } else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- // hasLine(lineCode+line) + target(no bch) + text
- if (toBlockGlobalNumber == null) throw new IllegalArgumentException("EDIT_POST missing toBlockGlobalNumber");
- if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("EDIT_POST toBlockHash32 != 32");
-
- int cap = (4 + 4 + 32 + 4) + (4 + 32) + 2 + msgUtf8.length;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
- bb.putInt(lineCode);
- bb.putInt(prevLineNumber);
- bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- bb.putInt(thisLineNumber);
-
- bb.putInt(toBlockGlobalNumber);
- bb.put(toBlockHash32);
-
- bb.putShort((short) msgUtf8.length);
- bb.put(msgUtf8);
- return bb.array();
-
- } else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
- // target(with bch) + text
- if (toBlockchainName == null) throw new IllegalArgumentException("REPLY missing toBlockchainName");
- if (toBlockGlobalNumber == null) throw new IllegalArgumentException("REPLY missing toBlockGlobalNumber");
- if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("REPLY toBlockHash32 != 32");
-
- byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8);
- if (nameUtf8.length == 0 || nameUtf8.length > 255)
- throw new IllegalArgumentException("REPLY toBlockchainName utf8 len must be 1..255");
-
- int cap = 1 + nameUtf8.length + 4 + 32
- + 2 + msgUtf8.length;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
- bb.put((byte) nameUtf8.length);
- bb.put(nameUtf8);
- bb.putInt(toBlockGlobalNumber);
- bb.put(toBlockHash32);
-
- bb.putShort((short) msgUtf8.length);
- bb.put(msgUtf8);
- return bb.array();
-
- } else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
- // target(no bch) + text
- if (toBlockGlobalNumber == null) throw new IllegalArgumentException("EDIT_REPLY missing toBlockGlobalNumber");
- if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("EDIT_REPLY toBlockHash32 != 32");
-
- int cap = (4 + 32) + 2 + msgUtf8.length;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
- bb.putInt(toBlockGlobalNumber);
- bb.put(toBlockHash32);
-
- bb.putShort((short) msgUtf8.length);
- bb.put(msgUtf8);
- return bb.array();
-
- } else {
- throw new IllegalStateException("Unsupported Text subType: " + st);
- }
- }
-
- /* ===================================================================== */
- /* ========================== Helpers ================================== */
- /* ===================================================================== */
-
- private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) {
- int len = Short.toUnsignedInt(bb.getShort());
- if (len <= 0) throw new IllegalArgumentException(fieldName + " is empty");
- if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")");
-
- byte[] bytes = new byte[len];
- bb.get(bytes);
-
- var decoder = StandardCharsets.UTF_8.newDecoder()
- .onMalformedInput(CodingErrorAction.REPORT)
- .onUnmappableCharacter(CodingErrorAction.REPORT);
-
- try {
- String s = decoder.decode(ByteBuffer.wrap(bytes)).toString();
- if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank");
- return s;
- } catch (CharacterCodingException e) {
- throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
- }
- }
-
- private static void ensureMin(ByteBuffer bb, int need, String msg) {
- if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")");
- }
-
- private static void ensureNoTail(ByteBuffer bb, String ctx) {
- if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining());
- }
-
- /* ====================== BodyHasLine ====================== */
- @Override public int lineCode() { return lineCode; }
- @Override public int prevLineBlockGlobalNumber() { return prevLineNumber; }
- @Override public byte[] prevLineBlockHash32() {
- if (prevLineHash32 == null) return null;
- return Arrays.copyOf(prevLineHash32, 32);
- }
- @Override public int lineSeq() { return thisLineNumber; }
-
- /* ====================== BodyHasTarget ===================== */
- @Override public String toBchName() { return toBlockchainName; }
- @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
- @Override public byte[] toBlockHashBytes() { return toBlockHash32; }
-
- /* ===================================================================== */
- /* ===================== Удобные хелперы (для ChainState) =============== */
- /* ===================================================================== */
-
- /** true только для POST / EDIT_POST (т.е. это сообщение в линии канала). */
- public boolean isLineMessage() {
- int st = subType & 0xFFFF;
- return st == (MsgSubType.TEXT_POST & 0xFFFF)
- || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF);
- }
-
- /** true только для EDIT_POST / EDIT_REPLY. */
- public boolean isEditMessage() {
- int st = subType & 0xFFFF;
- return st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
- || st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF);
- }
-
- /** true только для REPLY / EDIT_REPLY (т.е. “не в линии”). */
- public boolean isReplyFamily() {
- int st = subType & 0xFFFF;
- return st == (MsgSubType.TEXT_REPLY & 0xFFFF)
- || st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF);
- }
-}
-package blockchain.body;
-
-import blockchain.MsgSubType;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.CodingErrorAction;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * TextLineBody — type=1, ver=1.
- *
- * subType:
- * - POST (10)
- * - EDIT_POST (11)
- *
- * Формат bodyBytes (BigEndian):
- *
- * POST:
- * [4] lineCode
- * [4] prevLineNumber
- * [32] prevLineHash32
- * [4] thisLineNumber
- * [2] textLenBytes (uint16)
- * [N] text UTF-8
- *
- * EDIT_POST:
- * [4] lineCode
- * [4] prevLineNumber
- * [32] prevLineHash32
- * [4] thisLineNumber
- * [4] toBlockGlobalNumber (int32)
- * [32] toBlockHash32
- * [2] textLenBytes (uint16)
- * [N] text UTF-8
- */
-public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarget {
-
- public static final short TYPE = 1;
- public static final short VER = 1;
-
- public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
-
- public final short subType; // из header
- public final short version; // из header (=1)
-
- // line
- public final int lineCode;
- public final int prevLineNumber;
- public final byte[] prevLineHash32; // 32 (может быть нули)
- public final int thisLineNumber;
-
- // target (только для EDIT_POST)
- public final Integer toBlockGlobalNumber; // nullable для POST
- public final byte[] toBlockHash32; // nullable для POST
-
- // text
- public final String message;
-
- /* ====================== parse from bytes ====================== */
-
- public TextLineBody(short subType, short version, byte[] bodyBytes) {
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
-
- this.subType = subType;
- this.version = version;
-
- if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
- throw new IllegalArgumentException("TextLineBody version must be 1, got=" + (this.version & 0xFFFF));
- }
-
- int st = this.subType & 0xFFFF;
- if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST, got subType=" + st);
- }
-
- ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
-
- // минимум line + textLen(2)
- ensureMin(bb, (4 + 4 + 32 + 4) + 2, "TextLineBody too short");
-
- this.lineCode = bb.getInt();
- this.prevLineNumber = bb.getInt();
-
- this.prevLineHash32 = new byte[32];
- bb.get(this.prevLineHash32);
-
- this.thisLineNumber = bb.getInt();
-
- if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- // нужен target
- ensureMin(bb, (4 + 32) + 2, "EDIT_POST missing target");
- int tgtNum = bb.getInt();
- byte[] tgtHash = new byte[32];
- bb.get(tgtHash);
-
- this.toBlockGlobalNumber = tgtNum;
- this.toBlockHash32 = tgtHash;
-
- } else {
- this.toBlockGlobalNumber = null;
- this.toBlockHash32 = null;
- }
-
- this.message = readStrictUtf8Len16(bb, "TextLineBody text");
-
- ensureNoTail(bb, "TextLineBody");
- }
-
- /* ====================== manual ctor ====================== */
-
- public TextLineBody(int lineCode,
- int prevLineNumber,
- byte[] prevLineHash32,
- int thisLineNumber,
- short subType,
- Integer toBlockGlobalNumber,
- byte[] toBlockHash32,
- String message) {
-
- Objects.requireNonNull(message, "message == null");
-
- int st = subType & 0xFFFF;
- if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST");
- }
-
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
- if (message.isBlank()) throw new IllegalArgumentException("message is blank");
-
- this.subType = subType;
- this.version = VER;
-
- this.lineCode = lineCode;
- this.prevLineNumber = prevLineNumber;
- this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- this.thisLineNumber = thisLineNumber;
-
- if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
- Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
- if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
-
- this.toBlockGlobalNumber = toBlockGlobalNumber;
- this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
- } else {
- this.toBlockGlobalNumber = null;
- this.toBlockHash32 = null;
- }
-
- this.message = message;
- }
-
- @Override
- public TextLineBody check() {
- int st = subType & 0xFFFF;
- if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF))
- throw new IllegalArgumentException("Bad TextLineBody subType: " + st);
-
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
- if (prevLineHash32 == null || prevLineHash32.length != 32)
- throw new IllegalArgumentException("prevLineHash32 invalid");
-
- if (message == null || message.isBlank())
- throw new IllegalArgumentException("Text message is blank");
-
- if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0)
- throw new IllegalArgumentException("EDIT_POST toBlockGlobalNumber invalid");
- if (toBlockHash32 == null || toBlockHash32.length != 32)
- throw new IllegalArgumentException("EDIT_POST toBlockHash32 invalid");
- } else {
- if (toBlockGlobalNumber != null || toBlockHash32 != null)
- throw new IllegalArgumentException("POST must not contain target fields");
- }
-
- return this;
- }
-
- @Override
- public byte[] toBytes() {
- byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
- if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
- if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
-
- int st = subType & 0xFFFF;
-
- int cap;
- if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
- cap = (4 + 4 + 32 + 4) + 2 + msgUtf8.length;
- } else {
- // EDIT_POST
- if (toBlockGlobalNumber == null) throw new IllegalArgumentException("EDIT_POST missing toBlockGlobalNumber");
- if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("EDIT_POST toBlockHash32 != 32");
- cap = (4 + 4 + 32 + 4) + (4 + 32) + 2 + msgUtf8.length;
- }
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
-
- bb.putInt(lineCode);
- bb.putInt(prevLineNumber);
- bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- bb.putInt(thisLineNumber);
-
- if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
- bb.putInt(toBlockGlobalNumber);
- bb.put(toBlockHash32);
- }
-
- bb.putShort((short) msgUtf8.length);
- bb.put(msgUtf8);
-
- return bb.array();
- }
-
- /* ====================== BodyHasLine ====================== */
- @Override public int lineCode() { return lineCode; }
- @Override public int prevLineBlockGlobalNumber() { return prevLineNumber; }
- @Override public byte[] prevLineBlockHash32() { return Arrays.copyOf(prevLineHash32, 32); }
- @Override public int lineSeq() { return thisLineNumber; }
-
- /* ====================== BodyHasTarget ===================== */
- @Override public String toBchName() { return null; } // по ТЗ: не хранить
- @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
- @Override public byte[] toBlockHashBytes() { return toBlockHash32; }
-
- /* ====================== helpers ====================== */
-
- public boolean isEditPost() {
- return (subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_POST & 0xFFFF);
- }
-
- private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) {
- int len = Short.toUnsignedInt(bb.getShort());
- if (len <= 0) throw new IllegalArgumentException(fieldName + " is empty");
- if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")");
-
- byte[] bytes = new byte[len];
- bb.get(bytes);
-
- var decoder = StandardCharsets.UTF_8.newDecoder()
- .onMalformedInput(CodingErrorAction.REPORT)
- .onUnmappableCharacter(CodingErrorAction.REPORT);
-
- try {
- String s = decoder.decode(ByteBuffer.wrap(bytes)).toString();
- if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank");
- return s;
- } catch (CharacterCodingException e) {
- throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
- }
- }
-
- private static void ensureMin(ByteBuffer bb, int need, String msg) {
- if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")");
- }
-
- private static void ensureNoTail(ByteBuffer bb, String ctx) {
- if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining());
- }
-}
-package blockchain.body;
-
-import blockchain.MsgSubType;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.CodingErrorAction;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * TextReplyBody — type=1, ver=1.
- *
- * subType:
- * - REPLY (20)
- * - EDIT_REPLY (21)
- *
- * Форматы bodyBytes (BigEndian):
- *
- * REPLY:
- * [1] toBlockchainNameLen (uint8)
- * [N] toBlockchainName UTF-8
- * [4] toBlockGlobalNumber
- * [32] toBlockHash32
- * [2] textLenBytes (uint16)
- * [M] text UTF-8
- *
- * EDIT_REPLY:
- * [4] toBlockGlobalNumber
- * [32] toBlockHash32
- * [2] textLenBytes (uint16)
- * [N] text UTF-8
- */
-public final class TextReplyBody implements BodyRecord, BodyHasTarget {
-
- public static final short TYPE = 1;
- public static final short VER = 1;
-
- public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
-
- public final short subType; // из header
- public final short version; // (=1)
-
- // target
- public final String toBlockchainName; // nullable для EDIT_REPLY
- public final int toBlockGlobalNumber;
- public final byte[] toBlockHash32; // 32
-
- // text
- public final String message;
-
- public TextReplyBody(short subType, short version, byte[] bodyBytes) {
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
-
- this.subType = subType;
- this.version = version;
-
- if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
- throw new IllegalArgumentException("TextReplyBody version must be 1, got=" + (this.version & 0xFFFF));
- }
-
- int st = this.subType & 0xFFFF;
- if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
- throw new IllegalArgumentException("TextReplyBody supports only REPLY/EDIT_REPLY, got subType=" + st);
- }
-
- ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
-
- if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
- // минимум: nameLen[1]+name[1]+global[4]+hash[32]+textLen[2]
- ensureMin(bb, 1 + 1 + 4 + 32 + 2, "REPLY too short");
-
- int nameLen = Byte.toUnsignedInt(bb.get());
- if (nameLen <= 0) throw new IllegalArgumentException("REPLY toBlockchainNameLen is 0");
- ensureMin(bb, nameLen + 4 + 32 + 2, "REPLY payload too short");
-
- byte[] nameBytes = new byte[nameLen];
- bb.get(nameBytes);
- this.toBlockchainName = new String(nameBytes, StandardCharsets.UTF_8);
-
- this.toBlockGlobalNumber = bb.getInt();
-
- this.toBlockHash32 = new byte[32];
- bb.get(this.toBlockHash32);
-
- } else {
- // EDIT_REPLY: target без имени
- ensureMin(bb, (4 + 32) + 2, "EDIT_REPLY too short");
-
- this.toBlockchainName = null;
- this.toBlockGlobalNumber = bb.getInt();
-
- this.toBlockHash32 = new byte[32];
- bb.get(this.toBlockHash32);
- }
-
- this.message = readStrictUtf8Len16(bb, "TextReplyBody text");
- ensureNoTail(bb, "TextReplyBody");
- }
-
- public TextReplyBody(short subType,
- int toBlockGlobalNumber,
- byte[] toBlockHash32,
- String toBlockchainName,
- String message) {
-
- Objects.requireNonNull(message, "message == null");
- Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
-
- int st = subType & 0xFFFF;
- if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
- throw new IllegalArgumentException("TextReplyBody supports only REPLY/EDIT_REPLY");
- }
-
- if (message.isBlank()) throw new IllegalArgumentException("message is blank");
- if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
-
- if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
- Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
- if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
- this.toBlockchainName = toBlockchainName;
- } else {
- // EDIT_REPLY: имя не хранить
- this.toBlockchainName = null;
- }
-
- this.subType = subType;
- this.version = VER;
-
- this.toBlockGlobalNumber = toBlockGlobalNumber;
- this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
-
- this.message = message;
- }
-
- @Override
- public TextReplyBody check() {
- int st = subType & 0xFFFF;
- if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF))
- throw new IllegalArgumentException("Bad TextReplyBody subType: " + st);
-
- if (message == null || message.isBlank())
- throw new IllegalArgumentException("Text message is blank");
-
- if (toBlockGlobalNumber < 0)
- throw new IllegalArgumentException("toBlockGlobalNumber < 0");
- if (toBlockHash32 == null || toBlockHash32.length != 32)
- throw new IllegalArgumentException("toBlockHash32 invalid");
-
- if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
- if (toBlockchainName == null || toBlockchainName.isBlank())
- throw new IllegalArgumentException("REPLY toBlockchainName is blank");
- } else {
- if (toBlockchainName != null)
- throw new IllegalArgumentException("EDIT_REPLY must not contain toBlockchainName");
- }
-
- return this;
- }
-
- @Override
- public byte[] toBytes() {
- byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
- if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
- if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
-
- int st = subType & 0xFFFF;
-
- if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
- if (toBlockchainName == null) throw new IllegalArgumentException("REPLY missing toBlockchainName");
-
- byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8);
- if (nameUtf8.length == 0 || nameUtf8.length > 255)
- throw new IllegalArgumentException("REPLY toBlockchainName utf8 len must be 1..255");
-
- int cap = 1 + nameUtf8.length + 4 + 32 + 2 + msgUtf8.length;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
- bb.put((byte) nameUtf8.length);
- bb.put(nameUtf8);
- bb.putInt(toBlockGlobalNumber);
- bb.put(toBlockHash32);
- bb.putShort((short) msgUtf8.length);
- bb.put(msgUtf8);
-
- return bb.array();
- }
-
- // EDIT_REPLY
- int cap = (4 + 32) + 2 + msgUtf8.length;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
- bb.putInt(toBlockGlobalNumber);
- bb.put(toBlockHash32);
- bb.putShort((short) msgUtf8.length);
- bb.put(msgUtf8);
-
- return bb.array();
- }
-
- /* ====================== BodyHasTarget ====================== */
-
- @Override public String toBchName() { return toBlockchainName; }
- @Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
- @Override public byte[] toBlockHashBytes() { return toBlockHash32; }
-
- public boolean isEditReply() {
- return (subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF);
- }
-
- /* ====================== helpers ====================== */
-
- private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) {
- int len = Short.toUnsignedInt(bb.getShort());
- if (len <= 0) throw new IllegalArgumentException(fieldName + " is empty");
- if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")");
-
- byte[] bytes = new byte[len];
- bb.get(bytes);
-
- var decoder = StandardCharsets.UTF_8.newDecoder()
- .onMalformedInput(CodingErrorAction.REPORT)
- .onUnmappableCharacter(CodingErrorAction.REPORT);
-
- try {
- String s = decoder.decode(ByteBuffer.wrap(bytes)).toString();
- if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank");
- return s;
- } catch (CharacterCodingException e) {
- throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
- }
- }
-
- private static void ensureMin(ByteBuffer bb, int need, String msg) {
- if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")");
- }
-
- private static void ensureNoTail(ByteBuffer bb, String ctx) {
- if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining());
- }
-}
-package blockchain.body;
-
-import blockchain.MsgSubType;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.CodingErrorAction;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * UserParamBody — type=4, ver=1 (в заголовке блока).
- *
- * subType (в заголовке блока):
- * 1 = TEXT_TEXT
- *
- * bodyBytes (BigEndian), новый формат:
- * [4] lineCode
- * [4] prevLineNumber
- * [32] prevLineHash32
- * [4] thisLineNumber
- *
- * [2] keyLenBytes (uint16)
- * [N] keyUtf8
- *
- * [2] valueLenBytes (uint16)
- * [M] valueUtf8
- */
-public final class UserParamBody implements BodyRecord, BodyHasLine {
-
- public static final short TYPE = 4;
- public static final short VER = 1;
-
- public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
-
- public final short subType; // из header
- public final short version; // из header
-
- // line
- public final int lineCode;
- public final int prevLineNumber;
- public final byte[] prevLineHash32;
- public final int thisLineNumber;
-
- public final String paramKey;
- public final String paramValue;
-
- public UserParamBody(short subType, short version, byte[] bodyBytes) {
- Objects.requireNonNull(bodyBytes, "bodyBytes == null");
-
- this.subType = subType;
- this.version = version;
-
- if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
- throw new IllegalArgumentException("UserParamBody version must be 1, got=" + (this.version & 0xFFFF));
- }
- if ((this.subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF)) {
- throw new IllegalArgumentException("Bad UserParam subType: " + (this.subType & 0xFFFF));
- }
-
- // минимум: lineCode(4)+line(4+32+4) + keyLen(2)+key(1) + valLen(2)+val(1)
- if (bodyBytes.length < 4 + (4 + 32 + 4) + 2 + 1 + 2 + 1) {
- throw new IllegalArgumentException("UserParamBody too short");
- }
-
- ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
-
- this.lineCode = bb.getInt();
-
- this.prevLineNumber = bb.getInt();
-
- this.prevLineHash32 = new byte[32];
- bb.get(this.prevLineHash32);
-
- this.thisLineNumber = bb.getInt();
-
- int keyLen = Short.toUnsignedInt(bb.getShort());
- if (keyLen <= 0) throw new IllegalArgumentException("paramKeyLen is 0");
- if (bb.remaining() < keyLen + 2) throw new IllegalArgumentException("UserParam key payload too short");
-
- byte[] keyBytes = new byte[keyLen];
- bb.get(keyBytes);
-
- int valLen = Short.toUnsignedInt(bb.getShort());
- if (valLen <= 0) throw new IllegalArgumentException("paramValueLen is 0");
- if (bb.remaining() < valLen) throw new IllegalArgumentException("UserParam value payload too short");
-
- byte[] valBytes = new byte[valLen];
- bb.get(valBytes);
-
- if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
-
- this.paramKey = strictUtf8(keyBytes, "paramKey");
- this.paramValue = strictUtf8(valBytes, "paramValue");
-
- if (this.paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
- if (this.paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
- }
-
- public UserParamBody(int lineCode,
- int prevLineNumber,
- byte[] prevLineHash32,
- int thisLineNumber,
- String paramKey,
- String paramValue) {
-
- Objects.requireNonNull(paramKey, "paramKey == null");
- Objects.requireNonNull(paramValue, "paramValue == null");
-
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
-
- this.subType = MsgSubType.USER_PARAM_TEXT_TEXT;
- this.version = VER;
-
- this.lineCode = lineCode;
- this.prevLineNumber = prevLineNumber;
- this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- this.thisLineNumber = thisLineNumber;
-
- if (paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
- if (paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
-
- this.paramKey = paramKey;
- this.paramValue = paramValue;
- }
-
- @Override
- public UserParamBody check() {
- if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
-
- if ((subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF))
- throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
-
- if (prevLineNumber == -1) {
- if (!isAllZero32(prevLineHash32)) throw new IllegalArgumentException("prevLineHash32 must be zero when prevLineNumber=-1");
- if (thisLineNumber != -1) throw new IllegalArgumentException("thisLineNumber must be -1 when prevLineNumber=-1");
- } else {
- if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
- }
-
- if (paramKey == null || paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
- if (paramValue == null || paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
-
- return this;
- }
-
- @Override
- public byte[] toBytes() {
- byte[] keyUtf8 = paramKey.getBytes(StandardCharsets.UTF_8);
- byte[] valUtf8 = paramValue.getBytes(StandardCharsets.UTF_8);
-
- if (keyUtf8.length == 0 || keyUtf8.length > 65535) throw new IllegalArgumentException("paramKey utf8 len must be 1..65535");
- if (valUtf8.length == 0 || valUtf8.length > 65535) throw new IllegalArgumentException("paramValue utf8 len must be 1..65535");
-
- int cap = 4 + (4 + 32 + 4)
- + 2 + keyUtf8.length
- + 2 + valUtf8.length;
-
- ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
-
- bb.putInt(lineCode);
-
- bb.putInt(prevLineNumber);
- bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
- bb.putInt(thisLineNumber);
-
- bb.putShort((short) keyUtf8.length);
- bb.put(keyUtf8);
-
- bb.putShort((short) valUtf8.length);
- bb.put(valUtf8);
-
- return bb.array();
- }
-
- private static String strictUtf8(byte[] bytes, String fieldName) {
- var decoder = StandardCharsets.UTF_8.newDecoder()
- .onMalformedInput(CodingErrorAction.REPORT)
- .onUnmappableCharacter(CodingErrorAction.REPORT);
-
- try {
- return decoder.decode(ByteBuffer.wrap(bytes)).toString();
- } catch (CharacterCodingException e) {
- throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
- }
- }
-
- private static boolean isAllZero32(byte[] b) {
- if (b == null || b.length != 32) return true;
- for (int i = 0; i < 32; i++) if (b[i] != 0) return false;
- return true;
- }
-
- /* ====================== BodyHasLine ====================== */
- @Override public int lineCode() { return lineCode; }
- @Override public int prevLineBlockGlobalNumber() { return prevLineNumber; }
- @Override public byte[] prevLineBlockHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
- @Override public int lineSeq() { return thisLineNumber; }
-}
-//package blockchain;
-//
-///**
-// * LineIndex — канонические номера линий блокчейна.
-// *
-// * Линия = независимая последовательность блоков внутри одного блокчейна.
-// */
-//public final class LineIndex {
-//
-// private LineIndex() {}
-//
-// public static final short HEADER = 0; // genesis / идентификация
-// public static final short TEXT = 1; // сообщения да надо
-// public static final short REACTION = 2; // реакции не надо
-// public static final short CONNECTION = 3; // связи (friend/contact/follow) да надо
-// public static final short USER_PARAM = 4; // параметры профиля да надо
-//}
-package blockchain;
-
-/**
- * MsgSubType — единое место для ВСЕХ subType сообщений (msg_sub_type).
- *
- * Правило:
- * - НИКАКИХ "магических чисел" subType по проекту.
- * - В тестах, в body-классах и в SQL-триггерах используем только эти константы.
- *
- * Важно:
- * - Значения менять после релиза нельзя (иначе сломается совместимость).
- *
- * =========================================================================
- * Про EDIT-типы (важные правила, чтобы не было “двойных правок”):
- *
- * 1) EDIT разрешён ТОЛЬКО автору (в своём блокчейне).
- * Никаких “я отредачу чужое” — нельзя.
- *
- * 2) EDIT всегда ссылается ТОЛЬКО на ОРИГИНАЛ:
- * - EDIT_POST -> на исходный POST
- * - EDIT_REPLY -> на исходный REPLY
- * НЕЛЬЗЯ ссылаться на предыдущий EDIT (цепочка edit-ов запрещена).
- *
- * 3) REPLY может ссылаться на блоки в чужих линиях / чужих каналах,
- * и существование цели на уровне check() не проверяется
- * (check() БД не видит). Если цели нет — “никто не увидит” и ок.
- * =========================================================================
- */
-public final class MsgSubType {
-
- private MsgSubType() {}
-
- /* ===================== HEADER (msg_type=0) ===================== */
-
- /** HeaderBody: subType всегда 0 (compat). */
- public static final short HEADER_COMPAT = 0;
- public static final short TECH_CREATE_CHANNEL = 1;
-
- /* ===================== TEXT (msg_type=1) ===================== */
-
- /**
- * POST — обычный пост в канале (в линии канала).
- * Имеет hasLine (prevLineNumber/prevLineHash32/thisLineNumber).
- */
- public static final short TEXT_POST = 10;
-
- /**
- * EDIT_POST — редактирование ПОСТА.
- * Имеет hasLine (принадлежит линии канала)
- * И имеет target на ОРИГИНАЛЬНЫЙ POST (без toBlockchainName).
- */
- public static final short TEXT_EDIT_POST = 11;
-
- /**
- * REPLY — ответ на сообщение.
- * НЕ в линии. Имеет target (toBlockchainName + blockNumber + hash32).
- * Может указывать на чужой блокчейн/чужую линию/чужой канал.
- */
- public static final short TEXT_REPLY = 20;
-
- /**
- * EDIT_REPLY — редактирование ОТВЕТА.
- * НЕ в линии. Имеет target на ОРИГИНАЛЬНЫЙ REPLY (без toBlockchainName).
- */
- public static final short TEXT_EDIT_REPLY = 21;
-
- /* ===================== REACTION (msg_type=2) ===================== */
-
- /** Лайк (LIKE). */
- public static final short REACTION_LIKE = 1;
-
- /* ===================== CONNECTION (msg_type=3) ===================== */
-
- /** Добавить в друзья. */
- public static final short CONNECTION_FRIEND = 10;
- /** Удалить из друзей. */
- public static final short CONNECTION_UNFRIEND = 11;
-
- /** Добавить в контакты. */
- public static final short CONNECTION_CONTACT = 20;
- /** Удалить из контактов. */
- public static final short CONNECTION_UNCONTACT = 21;
-
- /** Подписаться (follow). */
- public static final short CONNECTION_FOLLOW = 30;
- /** Отписаться (unfollow). */
- public static final short CONNECTION_UNFOLLOW = 31;
-
- /* ===================== USER_PARAM (msg_type=4) ===================== */
-
- /** Параметр профиля key/value (обе строки). */
- public static final short USER_PARAM_TEXT_TEXT = 1;
-}
-package utils.blockchain;
-
-import java.util.Objects;
-
-public final class BlockchainNameUtil {
-
- /**
- * Теперь новое правило:
- * blockchainName = login + "-"+ 3 цифры
- * Пример: "Dima-001" -> "Dima"
- *
- * Сколько символов отрезаем с конца blockchainName, чтобы получить login: "-001" = 4
- */
- public static final int BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN = 4;
-
- private BlockchainNameUtil() {}
-
- /**
- * Извлечь login из blockchainName: отрезаем последние 4 символа ("-NNN").
- * Пример: "Dima-001" -> "Dima"
- */
- public static String loginFromBlockchainName(String blockchainName) {
- if (blockchainName == null) return null;
-
- String s = blockchainName.trim();
- if (!hasDashAnd3DigitsSuffix(s)) return null;
-
- return s.substring(0, s.length() - BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN);
- }
-
- /**
- * Проверка правила:
- * - blockchainName должен оканчиваться на "-"+3 цифры
- * - blockchainName без суффикса "-NNN" должен равняться login
- *
- * ВАЖНО:
- * - сравнение строгое (case-sensitive)
- * - null/blank считаем невалидным
- */
- public static boolean isBlockchainNameMatchesLogin(String blockchainName, String login) {
- if (blockchainName == null || login == null) return false;
-
- String bn = blockchainName.trim();
- String lg = login.trim();
-
- if (bn.isEmpty() || lg.isEmpty()) return false;
- if (!hasDashAnd3DigitsSuffix(bn)) return false;
-
- String extracted = bn.substring(0, bn.length() - BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN);
- return Objects.equals(extracted, lg);
- }
-
- private static boolean hasDashAnd3DigitsSuffix(String s) {
- if (s == null) return false;
- int len = s.length();
- if (len <= BLOCKCHAIN_NAME_LOGIN_SUFFIX_LEN) return false;
-
- int dashPos = len - 4;
- if (s.charAt(dashPos) != '-') return false;
-
- char c1 = s.charAt(len - 3);
- char c2 = s.charAt(len - 2);
- char c3 = s.charAt(len - 1);
-
- return isDigit(c1) && isDigit(c2) && isDigit(c3);
- }
-
- private static boolean isDigit(char c) {
- return c >= '0' && c <= '9';
- }
-}
-package utils.files;
-
-import java.io.IOException;
-import java.nio.file.*;
-import java.util.Objects;
-
-/**
- * ===============================================================
- * FileStoreUtil — утилита работы с файлами в папке data/.
- *
- * Теперь поддерживает:
- * - основной файл блокчейна:
.bch
- * - временный файл блокчейна: .tmp_bch
- *
- * Важное:
- * - validateSimpleFileName() запрещает path traversal.
- * - atomicReplaceBlockchainFile(): пытается сделать ATOMIC_MOVE (если ФС поддерживает),
- * иначе делает обычный REPLACE_EXISTING move.
- * ===============================================================
- */
-public final class FileStoreUtil {
-
- /** Базовая папка для хранения всех файлов (создаётся автоматически). */
- public static final String DATA_DIR_NAME = "data";
-
- /** Расширение основного файла блокчейна. */
- public static final String BLOCKCHAIN_FILE_EXTENSION = ".bch";
-
- /** Расширение временного файла (старое+новое). */
- public static final String BLOCKCHAIN_TMP_EXTENSION = ".tmp_bch";
-
- private static final FileStoreUtil INSTANCE = new FileStoreUtil();
-
- private final Path dataDirPath;
-
- private FileStoreUtil() {
- this.dataDirPath = Paths.get(DATA_DIR_NAME);
- ensureDataDirExists();
- }
-
- public static FileStoreUtil getInstance() {
- return INSTANCE;
- }
-
- /* ===================================================================== */
- /* ======================== Базовые операции =========================== */
- /* ===================================================================== */
-
- public void newFile(String fileName, byte[] data) {
- Objects.requireNonNull(data, "data == null");
- Path target = resolveSafe(fileName);
- try {
- Files.write(target, data,
- StandardOpenOption.CREATE,
- StandardOpenOption.TRUNCATE_EXISTING,
- StandardOpenOption.WRITE);
- } catch (IOException e) {
- throw new IllegalStateException("Не удалось записать файл: " + target, e);
- }
- }
-
- public void addDataToFile(String fileName, byte[] data) {
- Objects.requireNonNull(data, "data == null");
- Path target = resolveSafe(fileName);
- try {
- Files.write(target, data,
- StandardOpenOption.CREATE,
- StandardOpenOption.WRITE,
- StandardOpenOption.APPEND);
- } catch (IOException e) {
- throw new IllegalStateException("Не удалось дописать файл: " + target, e);
- }
- }
-
- public byte[] readAllDataFromFile(String fileName) {
- Path target = resolveSafe(fileName);
- if (!Files.exists(target)) {
- throw new IllegalStateException("Файл не найден: " + target);
- }
- try {
- return Files.readAllBytes(target);
- } catch (IOException e) {
- throw new IllegalStateException("Не удалось прочитать файл: " + target, e);
- }
- }
-
- public boolean exists(String fileName) {
- Path target = resolveSafe(fileName);
- return Files.exists(target);
- }
-
- public long size(String fileName) {
- Path target = resolveSafe(fileName);
- try {
- return Files.size(target);
- } catch (IOException e) {
- throw new IllegalStateException("Не удалось получить размер файла: " + target, e);
- }
- }
-
- /* ===================================================================== */
- /* ===================== Блокчейн-файлы по имени ======================= */
- /* ===================================================================== */
-
- /** .bch */
- public String buildBlockchainFileName(String blockchainName) {
- validateSimpleFileName(blockchainName);
- return blockchainName + BLOCKCHAIN_FILE_EXTENSION;
- }
-
- /** .tmp_bch */
- public String buildBlockchainTmpFileName(String blockchainName) {
- validateSimpleFileName(blockchainName);
- return blockchainName + BLOCKCHAIN_TMP_EXTENSION;
- }
-
- public Path resolveBlockchainPath(String blockchainName) {
- return resolveSafe(buildBlockchainFileName(blockchainName));
- }
-
- public Path resolveBlockchainTmpPath(String blockchainName) {
- return resolveSafe(buildBlockchainTmpFileName(blockchainName));
- }
-
- public byte[] readBlockchain(String blockchainName) {
- return readAllDataFromFile(buildBlockchainFileName(blockchainName));
- }
-
- public void writeBlockchainTmp(String blockchainName, byte[] data) {
- newFile(buildBlockchainTmpFileName(blockchainName), data);
- }
-
- /**
- * Атомарно заменить основной файл блокчейна временным:
- * .tmp_bch -> .bch
- *
- * Стратегия:
- * 1) Пытаемся Files.move(..., ATOMIC_MOVE, REPLACE_EXISTING)
- * 2) Если ATOMIC_MOVE не поддерживается — делаем move с REPLACE_EXISTING без атомарности
- *
- * Важный нюанс:
- * - атомарность гарантируется только в пределах одной файловой системы.
- */
- public void atomicReplaceBlockchainFile(String blockchainName) {
- Path tmp = resolveBlockchainTmpPath(blockchainName);
- Path main = resolveBlockchainPath(blockchainName);
-
- if (!Files.exists(tmp)) {
- throw new IllegalStateException("TMP-файл не найден: " + tmp);
- }
-
- try {
- // 1) Пытаемся атомарный move
- Files.move(tmp, main,
- StandardCopyOption.REPLACE_EXISTING,
- StandardCopyOption.ATOMIC_MOVE);
- } catch (AtomicMoveNotSupportedException e) {
- // 2) Если ФС не поддерживает атомарный move — делаем обычный replace
- try {
- Files.move(tmp, main, StandardCopyOption.REPLACE_EXISTING);
- } catch (IOException ex) {
- throw new IllegalStateException("Не удалось заменить файл блокчейна (non-atomic): " + main, ex);
- }
- } catch (IOException e) {
- throw new IllegalStateException("Не удалось заменить файл блокчейна (atomic): " + main, e);
- }
- }
-
- /* ===================================================================== */
- /* ============================ Helpers ================================= */
- /* ===================================================================== */
-
- private void ensureDataDirExists() {
- try {
- if (!Files.exists(dataDirPath)) {
- Files.createDirectories(dataDirPath);
- }
- } catch (IOException e) {
- throw new IllegalStateException("Не удалось создать директорию хранения: " + dataDirPath, e);
- }
- }
-
- private Path resolveSafe(String fileName) {
- validateSimpleFileName(fileName);
- return dataDirPath.resolve(fileName);
- }
-
- /**
- * Валидация "простого имени":
- * - запрещаем слэши, обратные слэши, ".."
- * - запрещаем пустоту
- *
- * Важно: сюда у нас попадает и blockchainName (как часть имени файла),
- * поэтому blockchainName должен быть "простым": без путей.
- */
- private void validateSimpleFileName(String fileName) {
- Objects.requireNonNull(fileName, "fileName == null");
- if (fileName.isBlank()) {
- throw new IllegalArgumentException("Имя файла не должно быть пустым");
- }
- if (fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) {
- throw new IllegalArgumentException("Недопустимое имя файла: " + fileName);
- }
- }
-}
diff --git a/SHiNE-server/shine-server-blockchain/concat_to_file.sh b/SHiNE-server/shine-server-blockchain/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-blockchain/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-crypto/concat_to_file.sh b/SHiNE-server/shine-server-crypto/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-crypto/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-crypto/src/concat_to_file.sh b/SHiNE-server/shine-server-crypto/src/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-crypto/src/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-db/all_files.txt b/SHiNE-server/shine-server-db/all_files.txt
deleted file mode 100644
index b1511ea..0000000
--- a/SHiNE-server/shine-server-db/all_files.txt
+++ /dev/null
@@ -1,2832 +0,0 @@
-package shine.db.dao;
-
-import shine.db.SqliteDbController;
-import shine.db.entities.ActiveSessionEntry;
-
-import java.sql.*;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * DAO для таблицы active_sessions.
- *
- * Правило:
- * - методы с Connection НЕ закрывают соединение
- * - методы без Connection сами открывают и закрывают соединение
- */
-public final class ActiveSessionsDAO {
-
- private static volatile ActiveSessionsDAO instance;
- private final SqliteDbController db = SqliteDbController.getInstance();
-
- private ActiveSessionsDAO() { }
-
- public static ActiveSessionsDAO getInstance() {
- if (instance == null) {
- synchronized (ActiveSessionsDAO.class) {
- if (instance == null) instance = new ActiveSessionsDAO();
- }
- }
- return instance;
- }
-
- // -------------------- INSERT --------------------
-
- public void insert(Connection c, ActiveSessionEntry session) throws SQLException {
- String sql = """
- INSERT INTO active_sessions (
- session_id,
- login,
- session_key,
- storage_pwd,
- session_created_at_ms,
- last_authirificated_at_ms,
- push_endpoint,
- push_p256dh_key,
- push_auth_key,
- client_ip,
- client_info_from_client,
- client_info_from_request,
- user_language
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, session.getSessionId());
- ps.setString(2, session.getLogin());
- ps.setString(3, session.getSessionKey());
- ps.setString(4, session.getStoragePwd());
- ps.setLong(5, session.getSessionCreatedAtMs());
- ps.setLong(6, session.getLastAuthirificatedAtMs());
- ps.setString(7, session.getPushEndpoint());
- ps.setString(8, session.getPushP256dhKey());
- ps.setString(9, session.getPushAuthKey());
- ps.setString(10, session.getClientIp());
- ps.setString(11, session.getClientInfoFromClient());
- ps.setString(12, session.getClientInfoFromRequest());
- ps.setString(13, session.getUserLanguage());
- ps.executeUpdate();
- }
- }
-
- public void insert(ActiveSessionEntry session) throws SQLException {
- try (Connection c = db.getConnection()) {
- insert(c, session);
- }
- }
-
- // -------------------- SELECT --------------------
-
- public ActiveSessionEntry getBySessionId(Connection c, String sessionId) throws SQLException {
- String sql = """
- SELECT
- session_id,
- login,
- session_key,
- storage_pwd,
- session_created_at_ms,
- last_authirificated_at_ms,
- push_endpoint,
- push_p256dh_key,
- push_auth_key,
- client_ip,
- client_info_from_client,
- client_info_from_request,
- user_language
- FROM active_sessions
- WHERE session_id = ?
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, sessionId);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return mapRow(rs);
- }
- }
- }
-
- public ActiveSessionEntry getBySessionId(String sessionId) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getBySessionId(c, sessionId);
- }
- }
-
- public List getByLogin(Connection c, String login) throws SQLException {
- String sql = """
- SELECT
- session_id,
- login,
- session_key,
- storage_pwd,
- session_created_at_ms,
- last_authirificated_at_ms,
- push_endpoint,
- push_p256dh_key,
- push_auth_key,
- client_ip,
- client_info_from_client,
- client_info_from_request,
- user_language
- FROM active_sessions
- WHERE login = ?
- """;
-
- List result = new ArrayList<>();
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, login);
- try (ResultSet rs = ps.executeQuery()) {
- while (rs.next()) result.add(mapRow(rs));
- }
- }
-
- return result;
- }
-
- public List getByLogin(String login) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getByLogin(c, login);
- }
- }
-
- // -------------------- UPDATE --------------------
-
- public void updateLastAuthirificatedAtMs(Connection c, String sessionId, long lastAuthMs) throws SQLException {
- String sql = """
- UPDATE active_sessions
- SET last_authirificated_at_ms = ?
- WHERE session_id = ?
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setLong(1, lastAuthMs);
- ps.setString(2, sessionId);
- ps.executeUpdate();
- }
- }
-
- public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
- try (Connection c = db.getConnection()) {
- updateLastAuthirificatedAtMs(c, sessionId, lastAuthMs);
- }
- }
-
- public void updateOnRefresh(
- Connection c,
- String sessionId,
- long lastAuthMs,
- String clientIp,
- String clientInfoFromClient,
- String clientInfoFromRequest,
- String userLanguage
- ) throws SQLException {
-
- String sql = """
- UPDATE active_sessions
- SET
- last_authirificated_at_ms = ?,
- client_ip = ?,
- client_info_from_client = ?,
- client_info_from_request = ?,
- user_language = ?
- WHERE session_id = ?
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setLong(1, lastAuthMs);
- ps.setString(2, clientIp);
- ps.setString(3, clientInfoFromClient);
- ps.setString(4, clientInfoFromRequest);
- ps.setString(5, userLanguage);
- ps.setString(6, sessionId);
- ps.executeUpdate();
- }
- }
-
- public void updateOnRefresh(
- String sessionId,
- long lastAuthMs,
- String clientIp,
- String clientInfoFromClient,
- String clientInfoFromRequest,
- String userLanguage
- ) throws SQLException {
- try (Connection c = db.getConnection()) {
- updateOnRefresh(c, sessionId, lastAuthMs, clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage);
- }
- }
-
- // -------------------- DELETE --------------------
-
- public void deleteBySessionId(Connection c, String sessionId) throws SQLException {
- String sql = "DELETE FROM active_sessions WHERE session_id = ?";
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, sessionId);
- ps.executeUpdate();
- }
- }
-
- public void deleteBySessionId(String sessionId) throws SQLException {
- try (Connection c = db.getConnection()) {
- deleteBySessionId(c, sessionId);
- }
- }
-
- // -------------------- MAPPER --------------------
-
- private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException {
- String sessionId = rs.getString("session_id");
- String login = rs.getString("login");
- String sessionKey = rs.getString("session_key");
- String storagePwd = rs.getString("storage_pwd");
- long sessionCreatedAtMs = rs.getLong("session_created_at_ms");
- long lastAuthirificatedAtMs = rs.getLong("last_authirificated_at_ms");
- String pushEndpoint = rs.getString("push_endpoint");
- String pushP256dhKey = rs.getString("push_p256dh_key");
- String pushAuthKey = rs.getString("push_auth_key");
- String clientIp = rs.getString("client_ip");
- String clientInfoFromClient = rs.getString("client_info_from_client");
- String clientInfoFromRequest = rs.getString("client_info_from_request");
- String userLanguage = rs.getString("user_language");
-
- return new ActiveSessionEntry(
- sessionId,
- login,
- sessionKey,
- storagePwd,
- sessionCreatedAtMs,
- lastAuthirificatedAtMs,
- pushEndpoint,
- pushP256dhKey,
- pushAuthKey,
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
- }
-}
-package shine.db.dao;
-
-import shine.db.SqliteDbController;
-import shine.db.entities.BlockchainStateEntry;
-
-import java.sql.*;
-
-public final class BlockchainStateDAO {
-
- private static volatile BlockchainStateDAO instance;
- private final SqliteDbController db = SqliteDbController.getInstance();
-
- private BlockchainStateDAO() {}
-
- public static BlockchainStateDAO getInstance() {
- if (instance == null) {
- synchronized (BlockchainStateDAO.class) {
- if (instance == null) instance = new BlockchainStateDAO();
- }
- }
- return instance;
- }
-
- /** Получить по blockchainName без внешнего соединения. Сам открывает/закрывает. */
- public BlockchainStateEntry getByBlockchainName(String blockchainName) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getByBlockchainName(c, blockchainName);
- }
- }
-
- /** Получить по blockchainName с внешним соединением. Соединение НЕ закрывает. */
- public BlockchainStateEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
- String sql = """
- SELECT
- blockchain_name,
- login,
- blockchain_key,
- size_limit,
- file_size_bytes,
- last_block_number,
- last_block_hash,
- updated_at_ms
- FROM blockchain_state
- WHERE blockchain_name = ?
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, blockchainName);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return mapRow(rs);
- }
- }
- }
-
- /** UPSERT без внешнего соединения. Сам открывает/закрывает. */
- public void upsert(BlockchainStateEntry e) throws SQLException {
- try (Connection c = db.getConnection()) {
- upsert(c, e);
- }
- }
-
- /** UPSERT с внешним соединением. Соединение НЕ закрывает. */
- public void upsert(Connection c, BlockchainStateEntry e) throws SQLException {
- String sql = """
- INSERT INTO blockchain_state (
- blockchain_name,
- login,
- blockchain_key,
- size_limit,
- file_size_bytes,
- last_block_number,
- last_block_hash,
- updated_at_ms
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(blockchain_name)
- DO UPDATE SET
- login = excluded.login,
- blockchain_key = excluded.blockchain_key,
- size_limit = excluded.size_limit,
- file_size_bytes = excluded.file_size_bytes,
- last_block_number= excluded.last_block_number,
- last_block_hash = excluded.last_block_hash,
- updated_at_ms = excluded.updated_at_ms
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- int i = 1;
-
- ps.setString(i++, e.getBlockchainName());
- ps.setString(i++, nn(e.getLogin()));
- ps.setString(i++, nn(e.getBlockchainKey()));
-
- ps.setLong(i++, e.getSizeLimit());
- ps.setLong(i++, e.getFileSizeBytes());
-
- ps.setInt(i++, e.getLastBlockNumber());
- setBytesNullable(ps, i++, e.getLastBlockHash());
-
- ps.setLong(i++, e.getUpdatedAtMs());
-
- ps.executeUpdate();
- }
- }
-
- /**
- * Атомарно увеличить file_size_bytes на deltaBytes, но только если НЕ превысим size_limit.
- */
- public boolean tryIncreaseFileSizeWithinLimit(Connection c, String blockchainName, long deltaBytes, long nowMs) throws SQLException {
- String sql = """
- UPDATE blockchain_state
- SET
- file_size_bytes = file_size_bytes + ?,
- updated_at_ms = ?
- WHERE
- blockchain_name = ?
- AND (file_size_bytes + ?) <= size_limit
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setLong(1, deltaBytes);
- ps.setLong(2, nowMs);
- ps.setString(3, blockchainName);
- ps.setLong(4, deltaBytes);
- return ps.executeUpdate() > 0;
- }
- }
-
- private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
- BlockchainStateEntry e = new BlockchainStateEntry();
-
- e.setBlockchainName(rs.getString("blockchain_name"));
- e.setLogin(rs.getString("login"));
- e.setBlockchainKey(rs.getString("blockchain_key"));
-
- e.setSizeLimit(rs.getLong("size_limit"));
- e.setFileSizeBytes(rs.getLong("file_size_bytes"));
-
- e.setLastBlockNumber(rs.getInt("last_block_number"));
- e.setLastBlockHash(rs.getBytes("last_block_hash")); // nullable
-
- e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
-
- return e;
- }
-
- private static void setBytesNullable(PreparedStatement ps, int index, byte[] b) throws SQLException {
- if (b != null) ps.setBytes(index, b);
- else ps.setNull(index, Types.BLOB);
- }
-
- private static String nn(String s) { return s == null ? "" : s; }
-}
-package shine.db.dao;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import shine.db.SqliteDbController;
-import shine.db.entities.BlockEntry;
-
-import java.sql.*;
-
-/**
- * DAO для таблицы blocks (новый формат).
- *
- * Правило:
- * - методы с Connection НЕ закрывают соединение
- * - методы без Connection сами открывают и закрывают соединение
- *
- * Ключ:
- * - (bch_name, block_number) — уникальная пара в рамках общей БД сервера.
- */
-public final class BlocksDAO {
-
- private static volatile BlocksDAO instance;
- private final SqliteDbController db = SqliteDbController.getInstance();
- private static final Logger log = LoggerFactory.getLogger(BlocksDAO.class);
-
- private BlocksDAO() { }
-
- public static BlocksDAO getInstance() {
- if (instance == null) {
- synchronized (BlocksDAO.class) {
- if (instance == null) instance = new BlocksDAO();
- }
- }
- return instance;
- }
-
- // -------------------- INSERT --------------------
-
- /** Вставка с внешним соединением. Соединение НЕ закрывает. */
- public void insert(Connection c, BlockEntry e) throws SQLException {
- log.info("DBG BlockEntry: type={} sub={} lineCode={} prevLineNumber={} thisLineNumber={} prevLineHashLen={}",
- e.getMsgType(), e.getMsgSubType(),
- e.getLineCode(), e.getPrevLineNumber(), e.getThisLineNumber(),
- e.getPrevLineHash() == null ? null : e.getPrevLineHash().length
- );
-
- String sql = """
- INSERT INTO blocks (
- login,
- bch_name,
- block_number,
- msg_type,
- msg_sub_type,
- block_bytes,
- to_login,
- to_bch_name,
- to_block_number,
- to_block_hash,
- block_hash,
- block_signature,
- edited_by_block_number,
- line_code,
- prev_line_number,
- prev_line_hash,
- this_line_number
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- int i = 1;
-
- ps.setString(i++, e.getLogin());
- ps.setString(i++, e.getBchName());
- ps.setInt(i++, e.getBlockNumber());
-
- ps.setInt(i++, e.getMsgType());
- ps.setInt(i++, e.getMsgSubType());
-
- ps.setBytes(i++, e.getBlockBytes());
-
- if (e.getToLogin() != null) ps.setString(i++, e.getToLogin());
- else ps.setNull(i++, Types.VARCHAR);
-
- if (e.getToBchName() != null) ps.setString(i++, e.getToBchName());
- else ps.setNull(i++, Types.VARCHAR);
-
- if (e.getToBlockNumber() != null) ps.setInt(i++, e.getToBlockNumber());
- else ps.setNull(i++, Types.INTEGER);
-
- if (e.getToBlockHash() != null) ps.setBytes(i++, e.getToBlockHash());
- else ps.setNull(i++, Types.BLOB);
-
- ps.setBytes(i++, e.getBlockHash());
- ps.setBytes(i++, e.getBlockSignature());
-
- if (e.getEditedByBlockNumber() != null) ps.setInt(i++, e.getEditedByBlockNumber());
- else ps.setNull(i++, Types.INTEGER);
-
- // NEW: line_code
- if (e.getLineCode() != null) ps.setInt(i++, e.getLineCode());
- else ps.setNull(i++, Types.INTEGER);
-
- if (e.getPrevLineNumber() != null) ps.setInt(i++, e.getPrevLineNumber());
- else ps.setNull(i++, Types.INTEGER);
-
- if (e.getPrevLineHash() != null) ps.setBytes(i++, e.getPrevLineHash());
- else ps.setNull(i++, Types.BLOB);
-
- if (e.getThisLineNumber() != null) ps.setInt(i++, e.getThisLineNumber());
- else ps.setNull(i++, Types.INTEGER);
-
- ps.executeUpdate();
- }
- }
-
- /** Вставка без внешнего соединения. Сам открывает/закрывает. */
- public void insert(BlockEntry e) throws SQLException {
- try (Connection c = db.getConnection()) {
- insert(c, e);
- }
- }
-
- // -------------------- SELECT: HASH BY NUMBER --------------------
-
- /** Получить block_hash по (bch_name, block_number). Нужен для линейной проверки. */
- public byte[] getHashByNumber(Connection c, String bchName, int blockNumber) throws SQLException {
- String sql = """
- SELECT block_hash
- FROM blocks
- WHERE bch_name = ? AND block_number = ?
- LIMIT 1
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, bchName);
- ps.setInt(2, blockNumber);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return rs.getBytes("block_hash");
- }
- }
- }
-
- public byte[] getHashByNumber(String bchName, int blockNumber) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getHashByNumber(c, bchName, blockNumber);
- }
- }
-
- // -------------------- SELECT: FULL ENTRY --------------------
-
- public BlockEntry getByNumber(Connection c, String bchName, int blockNumber) throws SQLException {
- String sql = """
- SELECT
- login,
- bch_name,
- block_number,
- msg_type,
- msg_sub_type,
- block_bytes,
- to_login,
- to_bch_name,
- to_block_number,
- to_block_hash,
- block_hash,
- block_signature,
- edited_by_block_number,
- line_code,
- prev_line_number,
- prev_line_hash,
- this_line_number
- FROM blocks
- WHERE bch_name = ? AND block_number = ?
- LIMIT 1
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, bchName);
- ps.setInt(2, blockNumber);
-
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return mapRow(rs);
- }
- }
- }
-
- public BlockEntry getByNumber(String bchName, int blockNumber) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getByNumber(c, bchName, blockNumber);
- }
- }
-
- // -------------------- INTERNAL --------------------
-
- private BlockEntry mapRow(ResultSet rs) throws SQLException {
- BlockEntry e = new BlockEntry();
-
- e.setLogin(rs.getString("login"));
- e.setBchName(rs.getString("bch_name"));
- e.setBlockNumber(rs.getInt("block_number"));
-
- e.setMsgType(rs.getInt("msg_type"));
- e.setMsgSubType(rs.getInt("msg_sub_type"));
-
- e.setBlockBytes(rs.getBytes("block_bytes"));
-
- String toLogin = rs.getString("to_login");
- if (rs.wasNull()) toLogin = null;
- e.setToLogin(toLogin);
-
- String toBchName = rs.getString("to_bch_name");
- if (rs.wasNull()) toBchName = null;
- e.setToBchName(toBchName);
-
- Integer toBlockNumber = (Integer) rs.getObject("to_block_number");
- e.setToBlockNumber(toBlockNumber);
-
- byte[] toHash = rs.getBytes("to_block_hash");
- if (rs.wasNull()) toHash = null;
- e.setToBlockHash(toHash);
-
- e.setBlockHash(rs.getBytes("block_hash"));
- e.setBlockSignature(rs.getBytes("block_signature"));
-
- Integer editedBy = (Integer) rs.getObject("edited_by_block_number");
- e.setEditedByBlockNumber(editedBy);
-
- // NEW: line_code
- Integer lineCode = (Integer) rs.getObject("line_code");
- e.setLineCode(lineCode);
-
- Integer prevLn = (Integer) rs.getObject("prev_line_number");
- e.setPrevLineNumber(prevLn);
-
- byte[] prevLh = rs.getBytes("prev_line_hash");
- if (rs.wasNull()) prevLh = null;
- e.setPrevLineHash(prevLh);
-
- Integer thisLn = (Integer) rs.getObject("this_line_number");
- e.setThisLineNumber(thisLn);
-
- return e;
- }
-}
-package shine.db.dao;
-
-import shine.db.SqliteDbController;
-import shine.db.entities.IpGeoCacheEntry;
-
-import java.sql.*;
-
-/**
- * DAO для таблицы ip_geo_cache.
- *
- * Таблица:
- * - ip TEXT PRIMARY KEY
- * - geo TEXT
- * - updated_at_ms INTEGER NOT NULL
- *
- * Правило:
- * - методы с Connection НЕ закрывают соединение
- * - методы без Connection сами открывают и закрывают соединение
- */
-public final class IpGeoCacheDAO {
-
- private static volatile IpGeoCacheDAO instance;
- private final SqliteDbController db = SqliteDbController.getInstance();
-
- private IpGeoCacheDAO() { }
-
- public static IpGeoCacheDAO getInstance() {
- if (instance == null) {
- synchronized (IpGeoCacheDAO.class) {
- if (instance == null) instance = new IpGeoCacheDAO();
- }
- }
- return instance;
- }
-
- // -------------------- UPSERT --------------------
-
- /** UPSERT с внешним соединением. Соединение НЕ закрывает. */
- public void upsert(Connection c, IpGeoCacheEntry entry) throws SQLException {
- String sql = """
- INSERT INTO ip_geo_cache (ip, geo, updated_at_ms)
- VALUES (?, ?, ?)
- ON CONFLICT(ip)
- DO UPDATE SET
- geo = excluded.geo,
- updated_at_ms = excluded.updated_at_ms
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, entry.getIp());
- ps.setString(2, entry.getGeo());
- ps.setLong(3, entry.getUpdatedAtMs());
- ps.executeUpdate();
- }
- }
-
- /** UPSERT без внешнего соединения. Сам открывает/закрывает. */
- public void upsert(IpGeoCacheEntry entry) throws SQLException {
- try (Connection c = db.getConnection()) {
- upsert(c, entry);
- }
- }
-
- // -------------------- SELECT --------------------
-
- /** Получить по IP с внешним соединением. Соединение НЕ закрывает. */
- public IpGeoCacheEntry getByIp(Connection c, String ip) throws SQLException {
- String sql = """
- SELECT ip, geo, updated_at_ms
- FROM ip_geo_cache
- WHERE ip = ?
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, ip);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return mapRow(rs);
- }
- }
- }
-
- /** Получить по IP без внешнего соединения. Сам открывает/закрывает. */
- public IpGeoCacheEntry getByIp(String ip) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getByIp(c, ip);
- }
- }
-
- // -------------------- DELETE --------------------
-
- /** Удалить старые записи с внешним соединением. Соединение НЕ закрывает. */
- public int deleteOlderThan(Connection c, long thresholdMs) throws SQLException {
- String sql = "DELETE FROM ip_geo_cache WHERE updated_at_ms < ?";
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setLong(1, thresholdMs);
- return ps.executeUpdate();
- }
- }
-
- /** Удалить старые записи без внешнего соединения. Сам открывает/закрывает. */
- public int deleteOlderThan(long thresholdMs) throws SQLException {
- try (Connection c = db.getConnection()) {
- return deleteOlderThan(c, thresholdMs);
- }
- }
-
- // -------------------- MAPPER --------------------
-
- private IpGeoCacheEntry mapRow(ResultSet rs) throws SQLException {
- String ip = rs.getString("ip");
- String geo = rs.getString("geo");
- long updatedAtMs = rs.getLong("updated_at_ms");
- return new IpGeoCacheEntry(ip, geo, updatedAtMs);
- }
-}
-package shine.db.dao;
-
-import shine.db.SqliteDbController;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.*;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * SolanaUsersDAO — локальная таблица пользователей из Solana.
- *
- * Таблица: solana_users
- *
- * Колонки:
- * - login TEXT PRIMARY KEY (COLLATE NOCASE)
- * - blockchain_name TEXT NOT NULL
- * - solana_key TEXT NOT NULL
- * - blockchain_key TEXT NOT NULL
- * - device_key TEXT NOT NULL
- *
- * Правило работы с соединениями:
- * - методы с Connection НЕ закрывают соединение
- * - методы без Connection сами открывают и закрывают соединение
- */
-public final class SolanaUsersDAO {
-
- private static volatile SolanaUsersDAO instance;
- private final SqliteDbController db = SqliteDbController.getInstance();
-
- private SolanaUsersDAO() {}
-
- public static SolanaUsersDAO getInstance() {
- if (instance == null) {
- synchronized (SolanaUsersDAO.class) {
- if (instance == null) instance = new SolanaUsersDAO();
- }
- }
- return instance;
- }
-
- // -------------------- INSERT --------------------
-
- /** Вставка с внешним соединением. Соединение НЕ закрывает. */
- public void insert(Connection c, SolanaUserEntry user) throws SQLException {
- String sql = """
- INSERT INTO solana_users (
- login, blockchain_name, solana_key, blockchain_key, device_key
- ) VALUES (?, ?, ?, ?, ?)
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, user.getLogin());
- ps.setString(2, user.getBlockchainName());
- ps.setString(3, user.getSolanaKey());
- ps.setString(4, user.getBlockchainKey());
- ps.setString(5, user.getDeviceKey());
- ps.executeUpdate();
- }
- }
-
- /** Вставка без внешнего соединения. Сам открывает/закрывает. */
- public void insert(SolanaUserEntry user) throws SQLException {
- try (Connection c = db.getConnection()) {
- insert(c, user);
- }
- }
-
- // -------------------- EXISTS --------------------
-
- /** Проверка существования по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
- public boolean existsByLogin(Connection c, String login) throws SQLException {
- String sql = """
- SELECT 1
- FROM solana_users
- WHERE LOWER(login) = LOWER(?)
- LIMIT 1
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, login);
- try (ResultSet rs = ps.executeQuery()) {
- return rs.next();
- }
- }
- }
-
- /** Проверка существования по login (case-insensitive) без внешнего соединения. Сам открывает/закрывает. */
- public boolean existsByLogin(String login) throws SQLException {
- try (Connection c = db.getConnection()) {
- return existsByLogin(c, login);
- }
- }
-
- /** Проверка существования по blockchain_name (case-sensitive, как в БД) с внешним соединением. */
- public boolean existsByBlockchainName(Connection c, String blockchainName) throws SQLException {
- String sql = """
- SELECT 1
- FROM solana_users
- WHERE blockchain_name = ?
- LIMIT 1
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, blockchainName);
- try (ResultSet rs = ps.executeQuery()) {
- return rs.next();
- }
- }
- }
-
- /** Проверка существования по blockchain_name без внешнего соединения. */
- public boolean existsByBlockchainName(String blockchainName) throws SQLException {
- try (Connection c = db.getConnection()) {
- return existsByBlockchainName(c, blockchainName);
- }
- }
-
- // -------------------- SELECT --------------------
-
- /** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
- public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException {
- String sql = """
- SELECT
- login,
- blockchain_name,
- solana_key,
- blockchain_key,
- device_key
- FROM solana_users
- WHERE LOWER(login) = LOWER(?)
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, login);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return mapRow(rs);
- }
- }
- }
-
- /** Получить по login (case-insensitive) без внешнего соединения. Сам открывает/закрывает. */
- public SolanaUserEntry getByLogin(String login) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getByLogin(c, login);
- }
- }
-
- /** Получить по blockchain_name (case-sensitive) с внешним соединением. Соединение НЕ закрывает. */
- public SolanaUserEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
- String sql = """
- SELECT
- login,
- blockchain_name,
- solana_key,
- blockchain_key,
- device_key
- FROM solana_users
- WHERE blockchain_name = ?
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, blockchainName);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return mapRow(rs);
- }
- }
- }
-
- /** Получить по blockchain_name без внешнего соединения. */
- public SolanaUserEntry getByBlockchainName(String blockchainName) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getByBlockchainName(c, blockchainName);
- }
- }
-
- /** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
- public List searchByLoginPrefix(Connection c, String prefix) throws SQLException {
- String sql = """
- SELECT
- login,
- blockchain_name,
- solana_key,
- blockchain_key,
- device_key
- FROM solana_users
- WHERE LOWER(login) LIKE ?
- ORDER BY login
- LIMIT 5
- """;
-
- List result = new ArrayList<>();
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, prefix.toLowerCase() + "%");
- try (ResultSet rs = ps.executeQuery()) {
- while (rs.next()) result.add(mapRow(rs));
- }
- }
-
- return result;
- }
-
- /** Поиск по префиксу без внешнего соединения. Сам открывает/закрывает. */
- public List searchByLoginPrefix(String prefix) throws SQLException {
- try (Connection c = db.getConnection()) {
- return searchByLoginPrefix(c, prefix);
- }
- }
-
- // -------------------- MAPPER --------------------
-
- private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
- SolanaUserEntry e = new SolanaUserEntry();
-
- e.setLogin(rs.getString("login"));
- e.setBlockchainName(rs.getString("blockchain_name"));
- e.setSolanaKey(rs.getString("solana_key"));
- e.setBlockchainKey(rs.getString("blockchain_key"));
- e.setDeviceKey(rs.getString("device_key"));
-
- return e;
- }
-}
-package shine.db.dao;
-
-import shine.db.MsgSubType;
-import shine.db.SqliteDbController;
-
-import java.sql.*;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * SubscriptionsDAO — агрегатный DAO для "каналов" (подписок).
- *
- * Возвращает по каждой активной подписке (FOLLOW) + "сам на себя":
- * - login цели (channelLogin)
- * - blockchainName цели (channelBchName)
- * - count публикаций (TEXT_NEW)
- * - last publication: bytes оригинального блока (для timestamp)
- * - last publication: bytes актуального блока (edit или orig) — для текста превью
- *
- * Важно:
- * - это НЕ таблица => сущность результата хранится вложенным классом.
- * - методы с Connection НЕ закрывают соединение
- * - методы без Connection сами открывают и закрывают соединение
- */
-public final class SubscriptionsDAO {
-
- private static volatile SubscriptionsDAO instance;
- private final SqliteDbController db = SqliteDbController.getInstance();
-
- private SubscriptionsDAO() {}
-
- public static SubscriptionsDAO getInstance() {
- if (instance == null) {
- synchronized (SubscriptionsDAO.class) {
- if (instance == null) instance = new SubscriptionsDAO();
- }
- }
- return instance;
- }
-
- /** Результат одной строки ("канал") для подписок. */
- public static final class ChannelRow {
-
- private final String channelLogin;
- private final String channelBchName;
-
- private final int publicationsCount;
-
- /** Последняя публикация: global number (nullable если публикаций нет). */
- private final Integer lastPublicationGlobalNumber;
-
- /** Байты оригинальной публикации (FULL bytes блока) — для timestamp (nullable). */
- private final byte[] lastPublicationBlockBytes;
-
- /** Если публикация редактировалась: global number edit-блока (nullable). */
- private final Integer lastEditGlobalNumber;
-
- /** Байты edit-блока (FULL bytes блока) (nullable). */
- private final byte[] lastEditBlockBytes;
-
- public ChannelRow(String channelLogin,
- String channelBchName,
- int publicationsCount,
- Integer lastPublicationGlobalNumber,
- byte[] lastPublicationBlockBytes,
- Integer lastEditGlobalNumber,
- byte[] lastEditBlockBytes) {
-
- this.channelLogin = channelLogin;
- this.channelBchName = channelBchName;
- this.publicationsCount = publicationsCount;
- this.lastPublicationGlobalNumber = lastPublicationGlobalNumber;
- this.lastPublicationBlockBytes = lastPublicationBlockBytes;
- this.lastEditGlobalNumber = lastEditGlobalNumber;
- this.lastEditBlockBytes = lastEditBlockBytes;
- }
-
- public String getChannelLogin() { return channelLogin; }
- public String getChannelBchName() { return channelBchName; }
-
- public int getPublicationsCount() { return publicationsCount; }
-
- public Integer getLastPublicationGlobalNumber() { return lastPublicationGlobalNumber; }
- public byte[] getLastPublicationBlockBytes() { return lastPublicationBlockBytes; }
-
- public Integer getLastEditGlobalNumber() { return lastEditGlobalNumber; }
- public byte[] getLastEditBlockBytes() { return lastEditBlockBytes; }
- }
-
- // В проекте msg_type=1 означает TEXT (у тебя это уже зафиксировано).
- private static final int MSG_TYPE_TEXT = 1;
-
- /**
- * Получить список подписок (активные FOLLOW) + "сам на себя" и по каждой:
- * - count публикаций (TEXT_NEW)
- * - последнюю публикацию (orig bytes) + её edit (если есть)
- *
- * Поведение при 0 публикаций:
- * - publications_count = 0
- * - last_pub_* = NULL
- * - last_edit_* = NULL
- */
- public List getSubscribedChannels(Connection c, String requesterLogin) throws SQLException {
-
- String sql = """
- WITH subs AS (
- -- 1) FOLLOW-каналы
- SELECT
- cs.to_login AS channel_login,
- cs.to_bch_name AS channel_bch_name
- FROM connections_state cs
- WHERE cs.login = ?
- AND cs.rel_type = ?
-
- UNION
-
- -- 2) self: все блокчейны пользователя (если их несколько)
- SELECT
- bs.login AS channel_login,
- bs.blockchain_name AS channel_bch_name
- FROM blockchain_state bs
- WHERE bs.login = ?
- ),
- pub_counts AS (
- SELECT
- b.login AS channel_login,
- b.bch_name AS channel_bch_name,
- COUNT(*) AS publications_count
- FROM blocks b
- JOIN subs s
- ON s.channel_login = b.login
- AND s.channel_bch_name = b.bch_name
- WHERE b.msg_type = ?
- AND b.msg_sub_type = ?
- GROUP BY b.login, b.bch_name
- ),
- last_pub AS (
- SELECT
- b.login AS channel_login,
- b.bch_name AS channel_bch_name,
- MAX(b.block_global_number) AS last_pub_global_number
- FROM blocks b
- JOIN subs s
- ON s.channel_login = b.login
- AND s.channel_bch_name = b.bch_name
- WHERE b.msg_type = ?
- AND b.msg_sub_type = ?
- GROUP BY b.login, b.bch_name
- ),
- last_pub_block AS (
- SELECT
- b.login AS channel_login,
- b.bch_name AS channel_bch_name,
- b.block_global_number AS last_pub_global_number,
- b.block_byte AS last_pub_block_bytes,
- b.edited_by_block_global_number AS last_edit_global_number
- FROM blocks b
- JOIN last_pub lp
- ON lp.channel_login = b.login
- AND lp.channel_bch_name = b.bch_name
- AND lp.last_pub_global_number = b.block_global_number
- ),
- last_edit_block AS (
- SELECT
- e.login AS channel_login,
- e.bch_name AS channel_bch_name,
- e.block_global_number AS last_edit_global_number,
- e.block_byte AS last_edit_block_bytes
- FROM blocks e
- JOIN last_pub_block p
- ON p.channel_login = e.login
- AND p.channel_bch_name = e.bch_name
- AND p.last_edit_global_number = e.block_global_number
- )
- SELECT
- s.channel_login,
- s.channel_bch_name,
- COALESCE(pc.publications_count, 0) AS publications_count,
- p.last_pub_global_number,
- p.last_pub_block_bytes,
- p.last_edit_global_number,
- e.last_edit_block_bytes
- FROM subs s
- LEFT JOIN pub_counts pc
- ON pc.channel_login = s.channel_login
- AND pc.channel_bch_name = s.channel_bch_name
- LEFT JOIN last_pub_block p
- ON p.channel_login = s.channel_login
- AND p.channel_bch_name = s.channel_bch_name
- LEFT JOIN last_edit_block e
- ON e.channel_login = s.channel_login
- AND e.channel_bch_name = s.channel_bch_name
- ORDER BY s.channel_login, s.channel_bch_name
- """;
-
- List out = new ArrayList<>();
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- int i = 1;
-
- // FOLLOW
- ps.setString(i++, requesterLogin);
- ps.setInt(i++, (int) MsgSubType.CONNECTION_FOLLOW);
-
- // self
- ps.setString(i++, requesterLogin);
-
- // pub_counts
- ps.setInt(i++, MSG_TYPE_TEXT);
- ps.setInt(i++, (int) MsgSubType.TEXT_NEW);
-
- // last_pub
- ps.setInt(i++, MSG_TYPE_TEXT);
- ps.setInt(i++, (int) MsgSubType.TEXT_NEW);
-
- try (ResultSet rs = ps.executeQuery()) {
- while (rs.next()) {
- String channelLogin = rs.getString("channel_login");
- String channelBchName = rs.getString("channel_bch_name");
-
- int publicationsCount = rs.getInt("publications_count");
-
- Integer lastPubGn = (Integer) rs.getObject("last_pub_global_number");
- byte[] lastPubBytes = rs.getBytes("last_pub_block_bytes");
-
- Integer lastEditGn = (Integer) rs.getObject("last_edit_global_number");
- byte[] lastEditBytes = rs.getBytes("last_edit_block_bytes");
-
- out.add(new ChannelRow(
- channelLogin,
- channelBchName,
- publicationsCount,
- lastPubGn,
- lastPubBytes,
- lastEditGn,
- lastEditBytes
- ));
- }
- }
- }
-
- return out;
- }
-
- /** Вариант без внешнего соединения. Сам открывает/закрывает. */
- public List getSubscribedChannels(String requesterLogin) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getSubscribedChannels(c, requesterLogin);
- }
- }
-}
-package shine.db.dao;
-
-import shine.db.SqliteDbController;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.*;
-
-/**
- * UserCreateDAO — атомарное добавление пользователя:
- * - solana_users (login, blockchain_name, solana_key, blockchain_key, device_key)
- * - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
- *
- * ВАЖНО:
- * - только INSERT (без перезаписи существующих записей)
- * - если login или blockchainName заняты — возвращаем false (пользователь уже есть/занято)
- */
-public final class UserCreateDAO {
-
- private static volatile UserCreateDAO instance;
- private final SqliteDbController db = SqliteDbController.getInstance();
- private final SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
-
- private UserCreateDAO() {}
-
- public static UserCreateDAO getInstance() {
- if (instance == null) {
- synchronized (UserCreateDAO.class) {
- if (instance == null) instance = new UserCreateDAO();
- }
- }
- return instance;
- }
-
- /**
- * @return true если добавили; false если занято (login уже есть или blockchainName уже существует).
- */
- public boolean insertUserWithBlockchain(
- String login,
- String blockchainName,
- String solanaKey,
- String blockchainKey,
- String deviceKey,
- long sizeLimit,
- long nowMs
- ) throws SQLException {
-
- try (Connection c = db.getConnection()) {
- boolean oldAuto = c.getAutoCommit();
- c.setAutoCommit(false);
-
- // BEGIN IMMEDIATE — чтобы сразу взять write-lock и не ловить гонки
- try (Statement st = c.createStatement()) {
- st.execute("BEGIN IMMEDIATE");
- }
-
- try {
- // 1) solana_users
- SolanaUserEntry u = new SolanaUserEntry();
- u.setLogin(login);
- u.setBlockchainName(blockchainName);
- u.setSolanaKey(solanaKey);
- u.setBlockchainKey(blockchainKey);
- u.setDeviceKey(deviceKey);
-
- usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint
-
- // 2) blockchain_state — строго INSERT, без UPSERT (иначе можно перезаписать существующую цепочку)
- insertBlockchainStateStrict(
- c,
- blockchainName,
- login,
- blockchainKey,
- sizeLimit,
- nowMs
- );
-
- c.commit();
- return true;
-
- } catch (SQLException e) {
- c.rollback();
-
- String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
- if (msg.contains("constraint")) {
- return false;
- }
- throw e;
-
- } finally {
- c.setAutoCommit(oldAuto);
- }
- }
- }
-
- private static void insertBlockchainStateStrict(
- Connection c,
- String blockchainName,
- String login,
- String blockchainKey,
- long sizeLimit,
- long nowMs
- ) throws SQLException {
-
- String sql = """
- INSERT INTO blockchain_state (
- blockchain_name,
- login,
- blockchain_key,
- size_limit,
- file_size_bytes,
- last_block_number,
- last_block_hash,
- updated_at_ms
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- int i = 1;
- ps.setString(i++, blockchainName);
- ps.setString(i++, login);
- ps.setString(i++, blockchainKey);
-
- ps.setLong(i++, sizeLimit);
- ps.setLong(i++, 0L);
-
- ps.setInt(i++, -1);
- ps.setNull(i++, Types.BLOB); // старт: блоков ещё нет
- ps.setLong(i++, nowMs);
-
- ps.executeUpdate(); // если blockchainName занят -> constraint (PK)
- }
- }
-}
-package shine.db.dao;
-
-import shine.db.SqliteDbController;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.*;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * UserParamsDAO — хранение сохранённых параметров пользователя.
- *
- * Правило:
- * - методы с Connection НЕ закрывают соединение
- * - методы без Connection сами открывают и закрывают соединение
- *
- * ЛОГИКА time_ms:
- * - БД принимает запись только если она "новее" (time_ms строго больше текущего).
- * - Реализовано атомарно одним SQL: UPSERT + WHERE users_params.time_ms < excluded.time_ms
- */
-public final class UserParamsDAO {
-
- private static volatile UserParamsDAO instance;
- private final SqliteDbController db = SqliteDbController.getInstance();
-
- private UserParamsDAO() { }
-
- public static UserParamsDAO getInstance() {
- if (instance == null) {
- synchronized (UserParamsDAO.class) {
- if (instance == null) instance = new UserParamsDAO();
- }
- }
- return instance;
- }
-
- // -------------------- UPSERT (IF NEWER) --------------------
-
- public int upsertIfNewer(Connection c, UserParamEntry e) throws SQLException {
- String sql = """
- INSERT INTO users_params (
- login,
- param,
- time_ms,
- value,
- device_key,
- signature
- ) VALUES (?, ?, ?, ?, ?, ?)
- ON CONFLICT(login, param)
- DO UPDATE SET
- time_ms = excluded.time_ms,
- value = excluded.value,
- device_key = excluded.device_key,
- signature = excluded.signature
- WHERE users_params.time_ms < excluded.time_ms
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, e.getLogin());
- ps.setString(2, e.getParam());
- ps.setLong(3, e.getTimeMs());
- ps.setString(4, e.getValue());
-
- if (e.getDeviceKey() != null) ps.setString(5, e.getDeviceKey());
- else ps.setNull(5, Types.VARCHAR);
-
- if (e.getSignature() != null) ps.setString(6, e.getSignature());
- else ps.setNull(6, Types.VARCHAR);
-
- return ps.executeUpdate();
- }
- }
-
- public int upsertIfNewer(UserParamEntry e) throws SQLException {
- try (Connection c = db.getConnection()) {
- return upsertIfNewer(c, e);
- }
- }
-
- // -------------------- SELECT --------------------
-
- public UserParamEntry getByLoginAndParam(Connection c, String login, String param) throws SQLException {
- String sql = """
- SELECT
- login,
- param,
- time_ms,
- value,
- device_key,
- signature
- FROM users_params
- WHERE login = ? AND param = ?
- LIMIT 1
- """;
-
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, login);
- ps.setString(2, param);
-
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return mapRow(rs);
- }
- }
- }
-
- public UserParamEntry getByLoginAndParam(String login, String param) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getByLoginAndParam(c, login, param);
- }
- }
-
- public List getByLogin(Connection c, String login) throws SQLException {
- String sql = """
- SELECT
- login,
- param,
- time_ms,
- value,
- device_key,
- signature
- FROM users_params
- WHERE login = ?
- ORDER BY time_ms DESC
- """;
-
- List list = new ArrayList<>();
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, login);
- try (ResultSet rs = ps.executeQuery()) {
- while (rs.next()) list.add(mapRow(rs));
- }
- }
- return list;
- }
-
- public List getByLogin(String login) throws SQLException {
- try (Connection c = db.getConnection()) {
- return getByLogin(c, login);
- }
- }
-
- // -------------------- MAPPER --------------------
-
- private static UserParamEntry mapRow(ResultSet rs) throws SQLException {
- UserParamEntry e = new UserParamEntry();
- e.setLogin(rs.getString("login"));
- e.setParam(rs.getString("param"));
- e.setTimeMs(rs.getLong("time_ms"));
- e.setValue(rs.getString("value"));
-
- String dk = rs.getString("device_key");
- if (rs.wasNull()) dk = null;
- e.setDeviceKey(dk);
-
- String sig = rs.getString("signature");
- if (rs.wasNull()) sig = null;
- e.setSignature(sig);
-
- return e;
- }
-}
-package shine.db;
-
-import utils.config.AppConfig;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.file.*;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-/**
- * DatabaseInitializer — создание новой SQLite-БД по схеме SHiNE.
- *
- * В этой версии:
- * - создаём ТОЛЬКО таблицы/индексы
- * - в конце вызываем DatabaseTriggersInstaller.createAllTriggers(st)
- *
- * v2 (sessions):
- * - active_sessions.session_pwd удалён
- * - active_sessions.session_key хранит публичный ключ сессии (sessionPubKeyB64)
- */
-public final class DatabaseInitializer {
-
- private DatabaseInitializer() {}
-
- /* ===================== TEXT (msg_type=1) ===================== */
-
- public static final short TEXT_NEW = 1;
- public static final short TEXT_REPLY = 2;
- public static final short TEXT_REPOST = 3;
- public static final short TEXT_EDIT = 10;
-
- /* ===================== REACTION (msg_type=2) ===================== */
-
- public static final short REACTION_LIKE = 1;
-
- /* ===================== CONNECTION (msg_type=3) ===================== */
- public static final short CONNECTION_FRIEND = 10;
- public static final short CONNECTION_UNFRIEND = 11;
-
- public static final short CONNECTION_CONTACT = 20;
- public static final short CONNECTION_UNCONTACT = 21;
-
- public static final short CONNECTION_FOLLOW = 30;
- public static final short CONNECTION_UNFOLLOW = 31;
-
- public static void createNewDB(String[] args) {
- AppConfig config = AppConfig.getInstance();
- String dbPath = config.getParam("db.path");
-
- if (dbPath == null || dbPath.isBlank()) {
- System.err.println("Параметр db.path не задан в application.properties");
- return;
- }
-
- Path dbFile = Paths.get(dbPath);
- try {
- Path parent = dbFile.getParent();
- if (parent != null && !Files.exists(parent)) {
- Files.createDirectories(parent);
- }
-
- if (Files.exists(dbFile)) {
- System.out.println("Файл базы данных уже существует: " + dbFile.toAbsolutePath());
- System.out.print("Пересоздать БД (СТАРАЯ БУДЕТ УДАЛЕНА)? [y/N]: ");
-
- BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
- String answer = reader.readLine();
- if (!"y".equalsIgnoreCase(answer) && !"yes".equalsIgnoreCase(answer)) {
- System.out.println("Операция отменена. БД не изменена.");
- return;
- }
-
- Files.delete(dbFile);
- System.out.println("Старый файл БД удалён.");
- }
-
- createSchema("jdbc:sqlite:" + dbPath);
- System.out.println("Новая БД успешно создана по пути: " + dbFile.toAbsolutePath());
-
- } catch (IOException e) {
- System.err.println("Ошибка работы с файлом БД: " + e.getMessage());
- } catch (SQLException e) {
- System.err.println("Ошибка создания схемы БД: " + e.getMessage());
- }
- }
-
- private static void createSchema(String jdbcUrl) throws SQLException {
- try {
- Class.forName("org.sqlite.JDBC");
- } catch (ClassNotFoundException e) {
- throw new RuntimeException("SQLite JDBC driver not found", e);
- }
-
- try (Connection conn = DriverManager.getConnection(jdbcUrl);
- Statement st = conn.createStatement()) {
-
- st.execute("PRAGMA foreign_keys = ON");
-
- // 1. solana_users
- // ВАЖНО:
- // - Все требуемые поля теперь лежат в solana_users:
- // login, blockchain_name, solana_key, blockchain_key, device_key
- // - Поиск по login в DAO сделан case-insensitive.
- // - Для защиты от дублей "Anya" и "anya" добавляем COLLATE NOCASE на PRIMARY KEY.
- st.executeUpdate("""
- CREATE TABLE IF NOT EXISTS solana_users (
- login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
- blockchain_name TEXT NOT NULL,
- solana_key TEXT NOT NULL,
- blockchain_key TEXT NOT NULL,
- device_key TEXT NOT NULL
- );
- """);
-
- st.executeUpdate("""
- CREATE UNIQUE INDEX IF NOT EXISTS uq_solana_users_blockchain_name
- ON solana_users (blockchain_name);
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_solana_users_login
- ON solana_users (login);
- """);
-
- // 2. active_sessions (v2)
- st.executeUpdate("""
- CREATE TABLE IF NOT EXISTS active_sessions (
- session_id TEXT NOT NULL PRIMARY KEY,
- login TEXT NOT NULL,
- session_key TEXT NOT NULL,
- storage_pwd TEXT NOT NULL,
- session_created_at_ms INTEGER NOT NULL,
- last_authirificated_at_ms INTEGER NOT NULL,
- push_endpoint TEXT,
- push_p256dh_key TEXT,
- push_auth_key TEXT,
- client_ip TEXT,
- client_info_from_client TEXT,
- client_info_from_request TEXT,
- user_language TEXT,
- FOREIGN KEY (login) REFERENCES solana_users(login)
- );
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_active_sessions_login
- ON active_sessions (login);
- """);
-
- // 3. users_params
- st.executeUpdate("""
- CREATE TABLE IF NOT EXISTS users_params (
- login TEXT NOT NULL,
- param TEXT NOT NULL,
- time_ms INTEGER NOT NULL,
- value TEXT NOT NULL,
- device_key TEXT,
- signature TEXT,
- FOREIGN KEY (login) REFERENCES solana_users(login),
- UNIQUE (login, param)
- );
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_users_params_login
- ON users_params (login);
- """);
-
- // 4. ip_geo_cache
- st.executeUpdate("""
- CREATE TABLE IF NOT EXISTS ip_geo_cache (
- ip TEXT NOT NULL PRIMARY KEY,
- geo TEXT,
- updated_at_ms INTEGER NOT NULL
- );
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_ip_geo_cache_updated_at
- ON ip_geo_cache (updated_at_ms);
- """);
-
- // 5. blockchain_state
- st.executeUpdate("""
- CREATE TABLE IF NOT EXISTS blockchain_state (
- blockchain_name TEXT NOT NULL PRIMARY KEY,
- login TEXT NOT NULL,
- blockchain_key TEXT NOT NULL,
-
- size_limit INTEGER NOT NULL,
- file_size_bytes INTEGER NOT NULL,
-
- last_block_number INTEGER NOT NULL,
- last_block_hash BLOB,
-
- updated_at_ms INTEGER NOT NULL,
-
- FOREIGN KEY (login) REFERENCES solana_users(login)
- );
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_blockchain_state_login
- ON blockchain_state (login);
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_blockchain_state_updated_at
- ON blockchain_state (updated_at_ms);
- """);
-
- // 6. blocks (+ line_code)
- st.executeUpdate("""
- CREATE TABLE IF NOT EXISTS blocks (
- login TEXT NOT NULL,
- bch_name TEXT NOT NULL,
- block_number INTEGER NOT NULL CHECK(block_number >= 0),
-
- msg_type INTEGER NOT NULL,
- msg_sub_type INTEGER NOT NULL,
-
- block_bytes BLOB NOT NULL,
-
- -- target (reply/like/edit и т.д.)
- to_login TEXT,
- to_bch_name TEXT,
- to_block_number INTEGER CHECK(to_block_number IS NULL OR to_block_number >= 0),
- to_block_hash BLOB,
-
- -- собственные данные
- block_hash BLOB NOT NULL,
- block_signature BLOB NOT NULL,
-
- -- если этот блок был изменён последним edit'ом
- edited_by_block_number INTEGER CHECK(edited_by_block_number IS NULL OR edited_by_block_number >= 0),
-
- -- линейность (опционально)
- line_code INTEGER CHECK(line_code IS NULL OR line_code >= 0),
- prev_line_number INTEGER CHECK(prev_line_number IS NULL OR prev_line_number >= 0),
- prev_line_hash BLOB,
- this_line_number INTEGER CHECK(this_line_number IS NULL OR this_line_number >= 0),
-
- FOREIGN KEY (login) REFERENCES solana_users(login),
- FOREIGN KEY (bch_name) REFERENCES blockchain_state(blockchain_name),
-
- UNIQUE (bch_name, block_number)
- );
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_blocks_by_chain_number
- ON blocks (bch_name, block_number);
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_blocks_to_target
- ON blocks (to_login, to_bch_name, to_block_number);
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_blocks_by_line
- ON blocks (bch_name, line_code, this_line_number);
- """);
-
- // 7) connections_state
- st.executeUpdate("""
- CREATE TABLE IF NOT EXISTS connections_state (
- login TEXT NOT NULL,
- rel_type INTEGER NOT NULL,
- to_login TEXT NOT NULL,
- to_bch_name TEXT NOT NULL,
- to_block_number INTEGER,
- to_block_hash BLOB,
-
- FOREIGN KEY (login) REFERENCES solana_users(login),
-
- UNIQUE (login, rel_type, to_login)
- );
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_connections_state_login
- ON connections_state (login);
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_connections_state_to_login
- ON connections_state (to_login);
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_connections_state_pair
- ON connections_state (login, to_login);
- """);
-
- // 8) message_stats
- st.executeUpdate("""
- CREATE TABLE IF NOT EXISTS message_stats (
- to_login TEXT NOT NULL,
- to_bch_name TEXT NOT NULL,
- to_block_number INTEGER NOT NULL,
- to_block_hash BLOB NOT NULL,
-
- likes_count INTEGER NOT NULL DEFAULT 0,
- replies_count INTEGER NOT NULL DEFAULT 0,
- edits_count INTEGER NOT NULL DEFAULT 0,
-
- UNIQUE (
- to_login,
- to_bch_name,
- to_block_number,
- to_block_hash
- )
- );
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_message_stats_target
- ON message_stats (to_bch_name, to_block_number, to_block_hash);
- """);
-
- st.executeUpdate("""
- CREATE INDEX IF NOT EXISTS idx_message_stats_login
- ON message_stats (to_login);
- """);
-
- DatabaseTriggersInstaller.createAllTriggers(st);
- }
- }
-}
-package shine.db;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-/**
- * DatabaseTriggersInstaller — устанавливает триггеры, которые поддерживают бизнес-логику БД.
- *
- * Мы специально сделали триггеры максимально "совместимыми":
- * - НЕТ динамических сообщений в RAISE(...): только фиксированные строки.
- * (Некоторые SQLite-сборки / просмотрщики падают на "||" внутри RAISE.)
- * - НЕТ UPSERT "ON CONFLICT DO UPDATE" — вместо него:
- * INSERT OR IGNORE + UPDATE
- * (Старые SQLite не знают UPSERT.)
- *
- * =============================================================================
- * ОПИСАНИЕ ТРИГГЕРОВ
- * =============================================================================
- *
- * [1] trg_blocks_line_integrity_bi (BEFORE INSERT ON blocks)
- * Контроль целостности "линий" (line_code / prev_line_number / prev_line_hash / this_line_number).
- *
- * Зачем это нужно:
- * - В каналах/ветках/действиях ты хочешь иметь "линейную" последовательность,
- * где каждый следующий блок явно ссылается на предыдущий блок линии
- * и подтверждает, что ссылка не подменена.
- *
- * Когда срабатывает:
- * - ТОЛЬКО если при вставке передано ХОТЯ БЫ ОДНО из line-полей.
- * - Если line-поля не переданы — триггер вообще не работает (это важно).
- *
- * Что проверяет:
- * A) line-поля допускаются только для msg_type:
- * 0 (TECH), 1 (TEXT), 3 (CONNECTION), 4 (USER_PARAM)
- * B) Если пришло хоть одно line-поле — обязаны прийти ВСЕ 4 (никаких "частичных")
- * C) prev-блок линии существует в той же цепочке bch_name
- * D) prev_hash совпадает с block_hash найденного prev-блока
- * E) line_code корректный:
- * - либо первый шаг после root: prev_line_number == line_code
- * - либо prev уже принадлежит этой линии: p.line_code == NEW.line_code
- * F) this_line_number:
- * - первый шаг после root:
- * TEXT: this_line_number = 0
- * TECH/CONNECTION/USER_PARAM: this_line_number = 1
- * - обычный шаг:
- * TEXT: допускаем same или +1 (чтобы "edit" мог не двигать шаг)
- * TECH/CONNECTION/USER_PARAM: строго prev.this + 1
- *
- * Какие ошибки кидает:
- * - LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE
- * - LINE_ERR_PARTIAL_FIELDS
- * - LINE_ERR_NO_PREV
- * - LINE_ERR_PREV_HASH_MISMATCH
- * - LINE_ERR_LINE_CODE_MISMATCH
- * - LINE_ERR_FIRST_STEP_BAD_THIS
- * - LINE_ERR_THIS_LINE_BAD_STEP
- *
- * [2] trg_blocks_connection_state_ai (AFTER INSERT ON blocks WHEN msg_type=3)
- * Поддерживает таблицу connections_state как "текущее состояние" отношений:
- * - FRIEND/CONTACT/FOLLOW -> добавить/обновить состояние
- * - UNFRIEND/UNCONTACT/UNFOLLOW -> удалить соответствующее "позитивное" состояние
- *
- * [3] trg_blocks_message_stats_like_ai (AFTER INSERT ON blocks WHEN msg_type=2 AND sub_type=LIKE)
- * Поддерживает likes_count в message_stats для цели (to_*).
- *
- * [4] trg_blocks_message_stats_reply_ai (AFTER INSERT ON blocks WHEN msg_type=1 AND sub_type=REPLY)
- * Поддерживает replies_count в message_stats.
- *
- * [5] trg_blocks_edit_apply_ai (AFTER INSERT ON blocks WHEN msg_type=1 AND sub_type=EDIT)
- * Логика edit:
- * - помечает исходный блок edited_by_block_number = NEW.block_number
- * - увеличивает edits_count в message_stats
- */
-public final class DatabaseTriggersInstaller {
-
- private DatabaseTriggersInstaller() {}
-
- public static void createAllTriggers(Statement st) throws SQLException {
- // На всякий случай убираем старые "криво названные" триггеры,
- // если они когда-то попадали в БД.
- st.executeUpdate("DROP TRIGGER IF EXISTS trg_block_lini_integriti_by;");
- st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_line_integrity_bi;");
-
- st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_connection_state_ai;");
- st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_message_stats_like_ai;");
- st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_message_stats_reply_ai;");
- st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_edit_apply_ai;");
-
- createLineIntegrityTrigger(st);
- createConnectionStateTrigger(st);
- createMessageStatsLikeTrigger(st);
- createMessageStatsReplyTrigger(st);
- createEditApplyTrigger(st);
- }
-
- private static void createLineIntegrityTrigger(Statement st) throws SQLException {
- st.executeUpdate("""
- CREATE TRIGGER IF NOT EXISTS trg_blocks_line_integrity_bi
- BEFORE INSERT ON blocks
- WHEN
- NEW.line_code IS NOT NULL
- OR NEW.prev_line_number IS NOT NULL
- OR NEW.prev_line_hash IS NOT NULL
- OR NEW.this_line_number IS NOT NULL
- BEGIN
- SELECT RAISE(ABORT, 'LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE')
- WHERE NOT (NEW.msg_type IN (0, 1, 3, 4));
-
- SELECT RAISE(ABORT, 'LINE_ERR_PARTIAL_FIELDS')
- WHERE NEW.line_code IS NULL
- OR NEW.prev_line_number IS NULL
- OR NEW.prev_line_hash IS NULL
- OR NEW.this_line_number IS NULL;
-
- SELECT RAISE(ABORT, 'LINE_ERR_NO_PREV')
- WHERE NOT EXISTS(
- SELECT 1
- FROM blocks p
- WHERE p.bch_name = NEW.bch_name
- AND p.block_number = NEW.prev_line_number
- LIMIT 1
- );
-
- SELECT RAISE(ABORT, 'LINE_ERR_PREV_HASH_MISMATCH')
- WHERE NOT EXISTS(
- SELECT 1
- FROM blocks p
- WHERE p.bch_name = NEW.bch_name
- AND p.block_number = NEW.prev_line_number
- AND p.block_hash = NEW.prev_line_hash
- LIMIT 1
- );
-
- SELECT RAISE(ABORT, 'LINE_ERR_LINE_CODE_MISMATCH')
- WHERE NEW.prev_line_number <> NEW.line_code
- AND NOT EXISTS(
- SELECT 1
- FROM blocks p
- WHERE p.bch_name = NEW.bch_name
- AND p.block_number = NEW.prev_line_number
- AND p.line_code = NEW.line_code
- LIMIT 1
- );
-
- SELECT RAISE(ABORT, 'LINE_ERR_FIRST_STEP_BAD_THIS')
- WHERE NEW.prev_line_number = NEW.line_code
- AND NEW.this_line_number <> (CASE WHEN NEW.msg_type = 1 THEN 0 ELSE 1 END);
-
- SELECT RAISE(ABORT, 'LINE_ERR_THIS_LINE_BAD_STEP')
- WHERE NEW.prev_line_number <> NEW.line_code
- AND NOT EXISTS(
- SELECT 1
- FROM blocks p
- WHERE p.bch_name = NEW.bch_name
- AND p.block_number = NEW.prev_line_number
- AND p.this_line_number IS NOT NULL
- AND (
- (NEW.msg_type = 1 AND
- (NEW.this_line_number = p.this_line_number OR NEW.this_line_number = p.this_line_number + 1)
- )
- OR
- (NEW.msg_type IN (0,3,4) AND NEW.this_line_number = p.this_line_number + 1)
- )
- LIMIT 1
- );
- END;
- """);
- }
-
- private static void createConnectionStateTrigger(Statement st) throws SQLException {
- int FRIEND = (int) DatabaseInitializer.CONNECTION_FRIEND;
- int CONTACT = (int) DatabaseInitializer.CONNECTION_CONTACT;
- int FOLLOW = (int) DatabaseInitializer.CONNECTION_FOLLOW;
-
- int UNFRIEND = (int) DatabaseInitializer.CONNECTION_UNFRIEND;
- int UNCONTACT = (int) DatabaseInitializer.CONNECTION_UNCONTACT;
- int UNFOLLOW = (int) DatabaseInitializer.CONNECTION_UNFOLLOW;
-
- st.executeUpdate("""
- CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
- AFTER INSERT ON blocks
- WHEN NEW.msg_type = 3
- BEGIN
- -- FRIEND/CONTACT/FOLLOW:
- -- 1) если записи нет — создаём
- INSERT OR IGNORE INTO connections_state (
- login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
- )
- SELECT
- NEW.login,
- NEW.msg_sub_type,
- NEW.to_login,
- NEW.to_bch_name,
- NEW.to_block_number,
- NEW.to_block_hash
- WHERE NEW.msg_sub_type IN (%d, %d, %d)
- AND NEW.to_login IS NOT NULL
- AND NEW.to_bch_name IS NOT NULL;
-
- -- 2) если запись есть — обновляем актуальные to_*
- UPDATE connections_state
- SET
- to_bch_name = NEW.to_bch_name,
- to_block_number = NEW.to_block_number,
- to_block_hash = NEW.to_block_hash
- WHERE login = NEW.login
- AND rel_type = NEW.msg_sub_type
- AND to_login = NEW.to_login
- AND NEW.msg_sub_type IN (%d, %d, %d)
- AND NEW.to_login IS NOT NULL
- AND NEW.to_bch_name IS NOT NULL;
-
- -- UNFRIEND/UNCONTACT/UNFOLLOW:
- -- удаляем соответствующее "позитивное" состояние
- DELETE FROM connections_state
- WHERE login = NEW.login
- AND to_login = NEW.to_login
- AND rel_type = CASE NEW.msg_sub_type
- WHEN %d THEN %d
- WHEN %d THEN %d
- WHEN %d THEN %d
- ELSE rel_type
- END
- AND NEW.msg_sub_type IN (%d, %d, %d);
- END;
- """.formatted(
- FRIEND, CONTACT, FOLLOW,
- FRIEND, CONTACT, FOLLOW,
-
- UNFRIEND, FRIEND,
- UNCONTACT, CONTACT,
- UNFOLLOW, FOLLOW,
-
- UNFRIEND, UNCONTACT, UNFOLLOW
- ));
- }
-
- private static void createMessageStatsLikeTrigger(Statement st) throws SQLException {
- int LIKE = (int) DatabaseInitializer.REACTION_LIKE;
-
- st.executeUpdate("""
- CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai
- AFTER INSERT ON blocks
- WHEN NEW.msg_type = 2 AND NEW.msg_sub_type = %d
- BEGIN
- -- создаём строку, если её не было
- INSERT OR IGNORE INTO message_stats (
- to_login, to_bch_name, to_block_number, to_block_hash,
- likes_count, replies_count, edits_count
- )
- SELECT
- NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
- 0, 0, 0
- WHERE NEW.to_login IS NOT NULL
- AND NEW.to_bch_name IS NOT NULL
- AND NEW.to_block_number IS NOT NULL
- AND NEW.to_block_hash IS NOT NULL;
-
- -- +1 like
- UPDATE message_stats
- SET likes_count = likes_count + 1
- WHERE to_login = NEW.to_login
- AND to_bch_name = NEW.to_bch_name
- AND to_block_number = NEW.to_block_number
- AND to_block_hash = NEW.to_block_hash
- AND NEW.to_login IS NOT NULL
- AND NEW.to_bch_name IS NOT NULL
- AND NEW.to_block_number IS NOT NULL
- AND NEW.to_block_hash IS NOT NULL;
- END;
- """.formatted(LIKE));
- }
-
- private static void createMessageStatsReplyTrigger(Statement st) throws SQLException {
- int REPLY = (int) DatabaseInitializer.TEXT_REPLY;
-
- st.executeUpdate("""
- CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai
- AFTER INSERT ON blocks
- WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d
- BEGIN
- INSERT OR IGNORE INTO message_stats (
- to_login, to_bch_name, to_block_number, to_block_hash,
- likes_count, replies_count, edits_count
- )
- SELECT
- NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
- 0, 0, 0
- WHERE NEW.to_login IS NOT NULL
- AND NEW.to_bch_name IS NOT NULL
- AND NEW.to_block_number IS NOT NULL
- AND NEW.to_block_hash IS NOT NULL;
-
- UPDATE message_stats
- SET replies_count = replies_count + 1
- WHERE to_login = NEW.to_login
- AND to_bch_name = NEW.to_bch_name
- AND to_block_number = NEW.to_block_number
- AND to_block_hash = NEW.to_block_hash
- AND NEW.to_login IS NOT NULL
- AND NEW.to_bch_name IS NOT NULL
- AND NEW.to_block_number IS NOT NULL
- AND NEW.to_block_hash IS NOT NULL;
- END;
- """.formatted(REPLY));
- }
-
- private static void createEditApplyTrigger(Statement st) throws SQLException {
- int EDIT = (int) DatabaseInitializer.TEXT_EDIT;
-
- st.executeUpdate("""
- CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai
- AFTER INSERT ON blocks
- WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d
- BEGIN
- -- 1) помечаем исходный блок, что его "перекрыл" этот edit
- UPDATE blocks
- SET edited_by_block_number = NEW.block_number
- WHERE login = NEW.login
- AND bch_name = NEW.bch_name
- AND block_number = NEW.to_block_number
- AND NEW.to_block_number IS NOT NULL;
-
- -- 2) создаём stats-строку если её не было
- INSERT OR IGNORE INTO message_stats (
- to_login, to_bch_name, to_block_number, to_block_hash,
- likes_count, replies_count, edits_count
- )
- SELECT
- NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
- 0, 0, 0
- WHERE NEW.to_login IS NOT NULL
- AND NEW.to_bch_name IS NOT NULL
- AND NEW.to_block_number IS NOT NULL
- AND NEW.to_block_hash IS NOT NULL;
-
- -- 3) +1 edit
- UPDATE message_stats
- SET edits_count = edits_count + 1
- WHERE to_login = NEW.to_login
- AND to_bch_name = NEW.to_bch_name
- AND to_block_number = NEW.to_block_number
- AND to_block_hash = NEW.to_block_hash
- AND NEW.to_login IS NOT NULL
- AND NEW.to_bch_name IS NOT NULL
- AND NEW.to_block_number IS NOT NULL
- AND NEW.to_block_hash IS NOT NULL;
- END;
- """.formatted(EDIT));
- }
-}
-package shine.db.entities;
-
-/**
- * Модель активной сессии (таблица active_sessions).
- */
-public class ActiveSessionEntry {
-
- private String sessionId;
- private String login;
-
- /** session_key: публичный ключ сессии (base64 от 32 байт). */
- private String sessionKey;
-
- private String storagePwd;
- private long sessionCreatedAtMs;
- private long lastAuthirificatedAtMs;
-
- private String pushEndpoint;
- private String pushP256dhKey;
- private String pushAuthKey;
-
- private String clientIp;
- private String clientInfoFromClient;
- private String clientInfoFromRequest;
- private String userLanguage;
-
- public ActiveSessionEntry() { }
-
- public ActiveSessionEntry(String sessionId,
- String login,
- String sessionKey,
- String storagePwd,
- long sessionCreatedAtMs,
- long lastAuthirificatedAtMs,
- String pushEndpoint,
- String pushP256dhKey,
- String pushAuthKey,
- String clientIp,
- String clientInfoFromClient,
- String clientInfoFromRequest,
- String userLanguage) {
- this.sessionId = sessionId;
- this.login = login;
- this.sessionKey = sessionKey;
- this.storagePwd = storagePwd;
- this.sessionCreatedAtMs = sessionCreatedAtMs;
- this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
- this.pushEndpoint = pushEndpoint;
- this.pushP256dhKey = pushP256dhKey;
- this.pushAuthKey = pushAuthKey;
- this.clientIp = clientIp;
- this.clientInfoFromClient = clientInfoFromClient;
- this.clientInfoFromRequest = clientInfoFromRequest;
- this.userLanguage = userLanguage;
- }
-
- public String getSessionId() { return sessionId; }
- public void setSessionId(String sessionId) { this.sessionId = sessionId; }
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getSessionKey() { return sessionKey; }
- public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; }
-
- public String getStoragePwd() { return storagePwd; }
- public void setStoragePwd(String storagePwd) { this.storagePwd = storagePwd; }
-
- public long getSessionCreatedAtMs() { return sessionCreatedAtMs; }
- public void setSessionCreatedAtMs(long sessionCreatedAtMs) { this.sessionCreatedAtMs = sessionCreatedAtMs; }
-
- public long getLastAuthirificatedAtMs() { return lastAuthirificatedAtMs; }
- public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) { this.lastAuthirificatedAtMs = lastAuthirificatedAtMs; }
-
- public String getPushEndpoint() { return pushEndpoint; }
- public void setPushEndpoint(String pushEndpoint) { this.pushEndpoint = pushEndpoint; }
-
- public String getPushP256dhKey() { return pushP256dhKey; }
- public void setPushP256dhKey(String pushP256dhKey) { this.pushP256dhKey = pushP256dhKey; }
-
- public String getPushAuthKey() { return pushAuthKey; }
- public void setPushAuthKey(String pushAuthKey) { this.pushAuthKey = pushAuthKey; }
-
- public String getClientIp() { return clientIp; }
- public void setClientIp(String clientIp) { this.clientIp = clientIp; }
-
- public String getClientInfoFromClient() { return clientInfoFromClient; }
- public void setClientInfoFromClient(String clientInfoFromClient) { this.clientInfoFromClient = clientInfoFromClient; }
-
- public String getClientInfoFromRequest() { return clientInfoFromRequest; }
- public void setClientInfoFromRequest(String clientInfoFromRequest) { this.clientInfoFromRequest = clientInfoFromRequest; }
-
- public String getUserLanguage() { return userLanguage; }
- public void setUserLanguage(String userLanguage) { this.userLanguage = userLanguage; }
-}
-package shine.db.entities;
-
-import java.util.Base64;
-
-/**
- * Агрегатная сущность текущего состояния блокчейна.
- *
- * ВАЖНО:
- * - Убраны все поля линий line0..7 (они больше не нужны).
- * - Оставляем:
- * last_block_number
- * last_block_hash
- *
- * Остальные поля (login, blockchain_key, лимиты) оставлены как в проекте,
- * потому что серверу они реально нужны (ключ подписи/лимит файла).
- */
-public final class BlockchainStateEntry {
-
- private String blockchainName;
- private String login;
-
- private String blockchainKey; // Base64(32)
-
- private long sizeLimit;
- private long fileSizeBytes;
-
- private int lastBlockNumber; // было last_global_number
- private byte[] lastBlockHash; // было last_global_hash (nullable)
-
- private long updatedAtMs;
-
- public BlockchainStateEntry() {}
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public byte[] getBlockchainKeyBytes() {
- if (blockchainKey == null) return null;
- String s = blockchainKey.trim();
- if (s.isEmpty()) return null;
- try {
- byte[] b = Base64.getDecoder().decode(s);
- return (b != null && b.length == 32) ? b : null;
- } catch (IllegalArgumentException e) {
- return null;
- }
- }
-
- public long getSizeLimit() { return sizeLimit; }
- public void setSizeLimit(long sizeLimit) { this.sizeLimit = sizeLimit; }
-
- public long getFileSizeBytes() { return fileSizeBytes; }
- public void setFileSizeBytes(long fileSizeBytes) { this.fileSizeBytes = fileSizeBytes; }
-
- public int getLastBlockNumber() { return lastBlockNumber; }
- public void setLastBlockNumber(int lastBlockNumber) { this.lastBlockNumber = lastBlockNumber; }
-
- public byte[] getLastBlockHash() { return lastBlockHash; }
- public void setLastBlockHash(byte[] lastBlockHash) { this.lastBlockHash = lastBlockHash; }
-
- public long getUpdatedAtMs() { return updatedAtMs; }
- public void setUpdatedAtMs(long updatedAtMs) { this.updatedAtMs = updatedAtMs; }
-}
-package shine.db.entities;
-
-/**
- * Запись блока (таблица blocks) — обновлённая модель под новый формат.
- *
- * Храним:
- * - login, bch_name (как было в проекте, чтобы не ломать общую БД)
- * - block_number (глобальный номер в этой цепочке)
- * - block_bytes (полный блок: preimage + signature)
- * - block_hash (32 байта вычисленный SHA-256(preimage))
- * - block_signature (64 байта)
- *
- * Опционально:
- * - line_code / prev_line_number / prev_line_hash / this_line_number
- *
- * Плюс поля индексации:
- * - msg_type / msg_sub_type
- * - to_* (если есть target)
- * - edited_by_block_number (для TEXT_EDIT)
- */
-public class BlockEntry {
-
- private String login;
- private String bchName;
-
- private int blockNumber;
-
- private int msgType;
- private int msgSubType;
-
- private byte[] blockBytes;
-
- private String toLogin;
- private String toBchName;
- private Integer toBlockNumber;
- private byte[] toBlockHash;
-
- private byte[] blockHash;
- private byte[] blockSignature;
-
- private Integer editedByBlockNumber;
-
- // NEW:
- private Integer lineCode;
-
- private Integer prevLineNumber;
- private byte[] prevLineHash;
- private Integer thisLineNumber;
-
- public BlockEntry() {}
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBchName() { return bchName; }
- public void setBchName(String bchName) { this.bchName = bchName; }
-
- public int getBlockNumber() { return blockNumber; }
- public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; }
-
- public int getMsgType() { return msgType; }
- public void setMsgType(int msgType) { this.msgType = msgType; }
-
- public int getMsgSubType() { return msgSubType; }
- public void setMsgSubType(int msgSubType) { this.msgSubType = msgSubType; }
-
- public byte[] getBlockBytes() { return blockBytes; }
- public void setBlockBytes(byte[] blockBytes) { this.blockBytes = blockBytes; }
-
- public String getToLogin() { return toLogin; }
- public void setToLogin(String toLogin) { this.toLogin = toLogin; }
-
- public String getToBchName() { return toBchName; }
- public void setToBchName(String toBchName) { this.toBchName = toBchName; }
-
- public Integer getToBlockNumber() { return toBlockNumber; }
- public void setToBlockNumber(Integer toBlockNumber) { this.toBlockNumber = toBlockNumber; }
-
- public byte[] getToBlockHash() { return toBlockHash; }
- public void setToBlockHash(byte[] toBlockHash) { this.toBlockHash = toBlockHash; }
-
- public byte[] getBlockHash() { return blockHash; }
- public void setBlockHash(byte[] blockHash) { this.blockHash = blockHash; }
-
- public byte[] getBlockSignature() { return blockSignature; }
- public void setBlockSignature(byte[] blockSignature) { this.blockSignature = blockSignature; }
-
- public Integer getEditedByBlockNumber() { return editedByBlockNumber; }
- public void setEditedByBlockNumber(Integer editedByBlockNumber) { this.editedByBlockNumber = editedByBlockNumber; }
-
- // NEW:
- public Integer getLineCode() { return lineCode; }
- public void setLineCode(Integer lineCode) { this.lineCode = lineCode; }
-
- public Integer getPrevLineNumber() { return prevLineNumber; }
- public void setPrevLineNumber(Integer prevLineNumber) { this.prevLineNumber = prevLineNumber; }
-
- public byte[] getPrevLineHash() { return prevLineHash; }
- public void setPrevLineHash(byte[] prevLineHash) { this.prevLineHash = prevLineHash; }
-
- public Integer getThisLineNumber() { return thisLineNumber; }
- public void setThisLineNumber(Integer thisLineNumber) { this.thisLineNumber = thisLineNumber; }
-}
-package shine.db.entities;
-
-/**
- * Запись в таблице ip_geo_cache.
- */
-public class IpGeoCacheEntry {
-
- private String ip;
- private String geo;
- private long updatedAtMs;
-
- public IpGeoCacheEntry() {
- }
-
- public IpGeoCacheEntry(String ip, String geo, long updatedAtMs) {
- this.ip = ip;
- this.geo = geo;
- this.updatedAtMs = updatedAtMs;
- }
-
- public String getIp() {
- return ip;
- }
-
- public void setIp(String ip) {
- this.ip = ip;
- }
-
- public String getGeo() {
- return geo;
- }
-
- public void setGeo(String geo) {
- this.geo = geo;
- }
-
- public long getUpdatedAtMs() {
- return updatedAtMs;
- }
-
- public void setUpdatedAtMs(long updatedAtMs) {
- this.updatedAtMs = updatedAtMs;
- }
-}
-package shine.db.entities;
-
-import java.util.Base64;
-
-/**
- * SolanaUserEntry — локальная запись пользователя из Solana.
- *
- * Таблица: solana_users
- *
- * Поля:
- * - login — PRIMARY KEY (TEXT) (case-insensitive на уровне COLLATE NOCASE)
- * - blockchain_name — TEXT NOT NULL
- * - solana_key — TEXT NOT NULL
- * - blockchain_key — TEXT NOT NULL
- * - device_key — TEXT NOT NULL
- */
-public class SolanaUserEntry {
-
- private String login;
-
- private String blockchainName;
-
- /** Ключ пользователя Solana (публичный ключ логина) */
- private String solanaKey;
-
- /** Ключ блокчейна (публичный ключ блокчейна) */
- private String blockchainKey;
-
- /** Ключ устройства (публичный ключ устройства) */
- private String deviceKey;
-
- public SolanaUserEntry() {}
-
- public SolanaUserEntry(String login,
- String blockchainName,
- String solanaKey,
- String blockchainKey,
- String deviceKey) {
- this.login = login;
- this.blockchainName = blockchainName;
- this.solanaKey = solanaKey;
- this.blockchainKey = blockchainKey;
- this.deviceKey = deviceKey;
- }
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-
- // оставляю этот метод как утилиту (иногда удобно), но он работает только для deviceKey:
- public byte[] getDeviceKeyByte() {
- if (deviceKey == null) return null;
- String s = deviceKey.trim();
- if (s.isEmpty()) return null;
-
- try {
- byte[] b = Base64.getDecoder().decode(s);
- if (b != null && b.length == 32) return b;
- } catch (IllegalArgumentException ignore) {}
-
- if (s.length() == 64 && s.matches("^[0-9a-fA-F]+$")) {
- byte[] out = new byte[32];
- for (int i = 0; i < 32; i++) {
- int hi = Character.digit(s.charAt(i * 2), 16);
- int lo = Character.digit(s.charAt(i * 2 + 1), 16);
- out[i] = (byte) ((hi << 4) | lo);
- }
- return out;
- }
-
- return null;
- }
-}
-package shine.db.entities;
-
-/**
- * UserParamEntry — сохранённый параметр пользователя.
- *
- * Таблица: users_params
- * - login TEXT NOT NULL
- * - param TEXT NOT NULL
- * - time_ms INTEGER NOT NULL
- * - value TEXT NOT NULL
- * - device_key TEXT NULL
- * - signature TEXT NULL
- */
-public class UserParamEntry {
-
- private String login;
- private String param;
- private long timeMs;
- private String value;
-
- private String deviceKey;
- private String signature;
-
- public UserParamEntry() {}
-
- public UserParamEntry(String login, String param, long timeMs, String value, String deviceKey, String signature) {
- this.login = login;
- this.param = param;
- this.timeMs = timeMs;
- this.value = value;
- this.deviceKey = deviceKey;
- this.signature = signature;
- }
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public long getTimeMs() { return timeMs; }
- public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package shine.db;
-
-/**
- * MsgSubType — единое место для ВСЕХ subType сообщений (msg_sub_type).
- *
- * ВАЖНО:
- * - Значения должны совпадать с body-классами (TextBody/ReactionBody/ConnectionBody/UserParamBody/HeaderBody).
- * - После релиза менять числа нельзя (иначе ломается совместимость данных).
- */
-public final class MsgSubType {
-
- private MsgSubType() {}
-
- /* ===================== HEADER (msg_type=0) ===================== */
-
- /** HeaderBody: subType всегда 0 (compat). */
- public static final short HEADER_COMPAT = 0;
-
- /* ===================== TEXT (msg_type=1) ===================== */
-
- /** Новая публикация. */
- public static final short TEXT_NEW = 1;
-
- /** Ответ (reply). */
- public static final short TEXT_REPLY = 2;
-
- /** Репост (repost). */
- public static final short TEXT_REPOST = 3;
-
- /** Редактирование (edit). */
- public static final short TEXT_EDIT = 10;
-
- /* ===================== REACTION (msg_type=2) ===================== */
-
- /** Лайк (LIKE). */
- public static final short REACTION_LIKE = 1;
-
- /* ===================== CONNECTION (msg_type=3) ===================== */
- /**
- * Совпадает с ConnectionBody:
- * SET: FRIEND=10, CONTACT=20, FOLLOW=30
- * UNSET: UNFRIEND=11, UNCONTACT=21, UNFOLLOW=31
- */
-
- /** Добавить в друзья. */
- public static final short CONNECTION_FRIEND = 10;
-
- /** Удалить из друзей. */
- public static final short CONNECTION_UNFRIEND = 11;
-
- /** Добавить в контакты. */
- public static final short CONNECTION_CONTACT = 20;
-
- /** Удалить из контактов. */
- public static final short CONNECTION_UNCONTACT = 21;
-
- /** Подписаться (follow). */
- public static final short CONNECTION_FOLLOW = 30;
-
- /** Отписаться (unfollow). */
- public static final short CONNECTION_UNFOLLOW = 31;
-
- /* ===================== USER_PARAM (msg_type=4) ===================== */
-
- /** Параметр профиля key/value (обе строки). */
- public static final short USER_PARAM_TEXT_TEXT = 1;
-
- /* ===================== РЕЗЕРВ НА БУДУЩЕЕ ===================== */
- // Если позже захочешь BLOCK/UNBLOCK — лучше добавить НОВЫЕ значения,
- // не трогая 10/20/30 и 11/21/31 (например, 40/41).
- // public static final short CONNECTION_BLOCK = 40;
- // public static final short CONNECTION_UNBLOCK = 41;
-}
-package shine.db;
-
-import utils.config.AppConfig;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public final class SqliteDbController {
-
- private static volatile SqliteDbController instance;
-
- private final String jdbcUrl;
-
- private SqliteDbController() {
- try {
- Class.forName("org.sqlite.JDBC");
- } catch (ClassNotFoundException e) {
- throw new RuntimeException("SQLite JDBC driver not found", e);
- }
-
- String dbPath = AppConfig.getInstance().getParam("db.path");
- if (dbPath == null || dbPath.isBlank()) {
- throw new RuntimeException("Config param 'db.path' is not set in application.properties");
- }
-
- Path dbFile = Paths.get(dbPath);
-
- if (!Files.exists(dbFile)) {
- System.out.println("[DB] Файл БД не найден: " + dbFile.toAbsolutePath());
- System.out.println("[DB] Создаём новую БД с помощью DatabaseInitializer...");
- DatabaseInitializer.createNewDB(new String[0]);
- }
-
- this.jdbcUrl = "jdbc:sqlite:" + dbPath;
- }
-
- public static SqliteDbController getInstance() {
- if (instance == null) {
- synchronized (SqliteDbController.class) {
- if (instance == null) {
- instance = new SqliteDbController();
- }
- }
- }
- return instance;
- }
-
- public Connection getConnection() throws SQLException {
- Connection conn = DriverManager.getConnection(jdbcUrl);
- conn.setAutoCommit(true);
-
- try (Statement st = conn.createStatement()) {
- st.execute("PRAGMA foreign_keys = ON");
- st.execute("PRAGMA journal_mode = WAL");
- st.execute("PRAGMA synchronous = NORMAL");
- st.execute("PRAGMA busy_timeout = 5000");
- }
-
- return conn;
- }
-
- public void close() {
- // no-op
- }
-}
diff --git a/SHiNE-server/shine-server-db/concat_to_file.sh b/SHiNE-server/shine-server-db/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-db/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java
index 2a8ef89..a1d781e 100644
--- a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java
+++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java
@@ -140,7 +140,7 @@ public final class DatabaseInitializer {
// 1. solana_users
// ВАЖНО:
// - Все требуемые поля теперь лежат в solana_users:
- // login, blockchain_name, solana_key, blockchain_key, device_key
+ // login, blockchain_name, solana_key, blockchain_key, client_key
// - Поиск по login в DAO сделан case-insensitive.
// - Для защиты от дублей "Anya" и "anya" добавляем COLLATE NOCASE на PRIMARY KEY.
st.executeUpdate("""
@@ -149,7 +149,7 @@ public final class DatabaseInitializer {
blockchain_name TEXT NOT NULL,
solana_key TEXT NOT NULL,
blockchain_key TEXT NOT NULL,
- device_key TEXT NOT NULL
+ client_key TEXT NOT NULL
);
""");
@@ -238,7 +238,7 @@ public final class DatabaseInitializer {
param TEXT NOT NULL,
time_ms INTEGER NOT NULL,
value TEXT NOT NULL,
- device_key TEXT,
+ client_key TEXT,
signature TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login),
UNIQUE (login, param)
diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java
index 3474a98..c9db7db 100644
--- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java
+++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java
@@ -17,7 +17,7 @@ import java.util.List;
* - blockchain_name TEXT NOT NULL
* - solana_key TEXT NOT NULL
* - blockchain_key TEXT NOT NULL
- * - device_key TEXT NOT NULL
+ * - client_key TEXT NOT NULL
*
* Правило работы с соединениями:
* - методы с Connection НЕ закрывают соединение
@@ -45,7 +45,7 @@ public final class SolanaUsersDAO {
public void insert(Connection c, SolanaUserEntry user) throws SQLException {
String sql = """
INSERT INTO solana_users (
- login, blockchain_name, solana_key, blockchain_key, device_key
+ login, blockchain_name, solana_key, blockchain_key, client_key
) VALUES (?, ?, ?, ?, ?)
""";
@@ -54,7 +54,7 @@ public final class SolanaUsersDAO {
ps.setString(2, user.getBlockchainName());
ps.setString(3, user.getSolanaKey());
ps.setString(4, user.getBlockchainKey());
- ps.setString(5, user.getDeviceKey());
+ ps.setString(5, user.getClientKey());
ps.executeUpdate();
}
}
@@ -126,7 +126,7 @@ public final class SolanaUsersDAO {
blockchain_name,
solana_key,
blockchain_key,
- device_key
+ client_key
FROM solana_users
WHERE LOWER(login) = LOWER(?)
""";
@@ -155,7 +155,7 @@ public final class SolanaUsersDAO {
blockchain_name,
solana_key,
blockchain_key,
- device_key
+ client_key
FROM solana_users
WHERE blockchain_name = ?
""";
@@ -184,7 +184,7 @@ public final class SolanaUsersDAO {
blockchain_name,
solana_key,
blockchain_key,
- device_key
+ client_key
FROM solana_users
WHERE LOWER(login) LIKE ?
ORDER BY login
@@ -219,7 +219,7 @@ public final class SolanaUsersDAO {
e.setBlockchainName(rs.getString("blockchain_name"));
e.setSolanaKey(rs.getString("solana_key"));
e.setBlockchainKey(rs.getString("blockchain_key"));
- e.setDeviceKey(rs.getString("device_key"));
+ e.setClientKey(rs.getString("client_key"));
return e;
}
diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java
index 431790b..8476f3a 100644
--- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java
+++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java
@@ -7,7 +7,7 @@ import java.sql.*;
/**
* UserCreateDAO — атомарное добавление пользователя:
- * - solana_users (login, blockchain_name, solana_key, blockchain_key, device_key)
+ * - solana_users (login, blockchain_name, solana_key, blockchain_key, client_key)
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
*
* ВАЖНО:
@@ -39,7 +39,7 @@ public final class UserCreateDAO {
String blockchainName,
String solanaKey,
String blockchainKey,
- String deviceKey,
+ String clientKey,
long sizeLimit,
long nowMs
) throws SQLException {
@@ -55,7 +55,7 @@ public final class UserCreateDAO {
u.setBlockchainName(blockchainName);
u.setSolanaKey(solanaKey);
u.setBlockchainKey(blockchainKey);
- u.setDeviceKey(deviceKey);
+ u.setClientKey(clientKey);
usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint
diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserParamsDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserParamsDAO.java
index 0cb87f2..fe20aeb 100644
--- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserParamsDAO.java
+++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserParamsDAO.java
@@ -43,14 +43,14 @@ public final class UserParamsDAO {
param,
time_ms,
value,
- device_key,
+ client_key,
signature
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(login, param)
DO UPDATE SET
time_ms = excluded.time_ms,
value = excluded.value,
- device_key = excluded.device_key,
+ client_key = excluded.client_key,
signature = excluded.signature
WHERE users_params.time_ms < excluded.time_ms
""";
@@ -61,7 +61,7 @@ public final class UserParamsDAO {
ps.setLong(3, e.getTimeMs());
ps.setString(4, e.getValue());
- if (e.getDeviceKey() != null) ps.setString(5, e.getDeviceKey());
+ if (e.getClientKey() != null) ps.setString(5, e.getClientKey());
else ps.setNull(5, Types.VARCHAR);
if (e.getSignature() != null) ps.setString(6, e.getSignature());
@@ -86,7 +86,7 @@ public final class UserParamsDAO {
param,
time_ms,
value,
- device_key,
+ client_key,
signature
FROM users_params
WHERE login = ? COLLATE NOCASE AND param = ?
@@ -117,7 +117,7 @@ public final class UserParamsDAO {
param,
time_ms,
value,
- device_key,
+ client_key,
signature
FROM users_params
WHERE login = ? COLLATE NOCASE
@@ -149,9 +149,9 @@ public final class UserParamsDAO {
e.setTimeMs(rs.getLong("time_ms"));
e.setValue(rs.getString("value"));
- String dk = rs.getString("device_key");
+ String dk = rs.getString("client_key");
if (rs.wasNull()) dk = null;
- e.setDeviceKey(dk);
+ e.setClientKey(dk);
String sig = rs.getString("signature");
if (rs.wasNull()) sig = null;
diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java
index b7dbd7c..7552faf 100644
--- a/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java
+++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java
@@ -12,7 +12,7 @@ import java.util.Base64;
* - blockchain_name — TEXT NOT NULL
* - solana_key — TEXT NOT NULL
* - blockchain_key — TEXT NOT NULL
- * - device_key — TEXT NOT NULL
+ * - client_key — TEXT NOT NULL
*/
public class SolanaUserEntry {
@@ -27,7 +27,7 @@ public class SolanaUserEntry {
private String blockchainKey;
/** Ключ устройства (публичный ключ устройства) */
- private String deviceKey;
+ private String clientKey;
public SolanaUserEntry() {}
@@ -35,12 +35,12 @@ public class SolanaUserEntry {
String blockchainName,
String solanaKey,
String blockchainKey,
- String deviceKey) {
+ String clientKey) {
this.login = login;
this.blockchainName = blockchainName;
this.solanaKey = solanaKey;
this.blockchainKey = blockchainKey;
- this.deviceKey = deviceKey;
+ this.clientKey = clientKey;
}
public String getLogin() { return login; }
@@ -55,13 +55,13 @@ public class SolanaUserEntry {
public String getBlockchainKey() { return blockchainKey; }
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
+ public String getClientKey() { return clientKey; }
+ public void setClientKey(String clientKey) { this.clientKey = clientKey; }
- // оставляю этот метод как утилиту (иногда удобно), но он работает только для deviceKey:
- public byte[] getDeviceKeyByte() {
- if (deviceKey == null) return null;
- String s = deviceKey.trim();
+ // оставляю этот метод как утилиту (иногда удобно), но он работает только для clientKey:
+ public byte[] getClientKeyByte() {
+ if (clientKey == null) return null;
+ String s = clientKey.trim();
if (s.isEmpty()) return null;
try {
diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/UserParamEntry.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/UserParamEntry.java
index 656614a..55fa207 100644
--- a/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/UserParamEntry.java
+++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/UserParamEntry.java
@@ -8,7 +8,7 @@ package shine.db.entities;
* - param TEXT NOT NULL
* - time_ms INTEGER NOT NULL
* - value TEXT NOT NULL
- * - device_key TEXT NULL
+ * - client_key TEXT NULL
* - signature TEXT NULL
*/
public class UserParamEntry {
@@ -18,17 +18,17 @@ public class UserParamEntry {
private long timeMs;
private String value;
- private String deviceKey;
+ private String clientKey;
private String signature;
public UserParamEntry() {}
- public UserParamEntry(String login, String param, long timeMs, String value, String deviceKey, String signature) {
+ public UserParamEntry(String login, String param, long timeMs, String value, String clientKey, String signature) {
this.login = login;
this.param = param;
this.timeMs = timeMs;
this.value = value;
- this.deviceKey = deviceKey;
+ this.clientKey = clientKey;
this.signature = signature;
}
@@ -44,8 +44,8 @@ public class UserParamEntry {
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
+ public String getClientKey() { return clientKey; }
+ public void setClientKey(String clientKey) { this.clientKey = clientKey; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
diff --git a/SHiNE-server/shine-server-log/concat_to_file.sh b/SHiNE-server/shine-server-log/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-log/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/all_files.txt b/SHiNE-server/shine-server-net-protocol/all_files.txt
deleted file mode 100644
index 8d18326..0000000
--- a/SHiNE-server/shine-server-net-protocol/all_files.txt
+++ /dev/null
@@ -1,4739 +0,0 @@
-package server.logic.ws_protocol.JSON;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-/**
- * Реестр активных подключений (только авторизованные).
- */
-public final class ActiveConnectionsRegistry {
-
- private static final Logger log = LoggerFactory.getLogger(ActiveConnectionsRegistry.class);
-
- private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry();
-
- public static ActiveConnectionsRegistry getInstance() {
- return INSTANCE;
- }
-
- private ActiveConnectionsRegistry() {
- // singleton
- }
-
- // sessionId (String) -> ConnectionContext
- private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>();
-
- // login (String) -> множество ConnectionContext для этого пользователя
- private final ConcurrentHashMap> byLogin = new ConcurrentHashMap<>();
-
- /**
- * Зарегистрировать авторизованное подключение.
- * Ожидается, что в ctx уже выставлены login и sessionId.
- */
- public void register(ConnectionContext ctx) {
- if (ctx == null) return;
-
- String sessionId = ctx.getSessionId();
- String login = ctx.getLogin();
-
- if (sessionId == null || sessionId.isBlank() || login == null || login.isBlank()) {
- log.debug("register skipped: bad ctx fields (login='{}', sessionId='{}')", login, sessionId);
- return;
- }
-
- // ✅ Если кто-то перерегистрировал тот же sessionId — вычищаем старый ctx из byLogin
- ConnectionContext prev = bySessionId.put(sessionId, ctx);
- if (prev != null && prev != ctx) {
- String prevLogin = prev.getLogin();
- if (prevLogin != null && !prevLogin.isBlank()) {
- Set prevSet = byLogin.get(prevLogin);
- if (prevSet != null) {
- prevSet.remove(prev);
- if (prevSet.isEmpty()) {
- byLogin.remove(prevLogin);
- }
- }
- }
- log.warn("sessionId reused: replaced previous ctx (sessionId={}, prevLogin={}, newLogin={})",
- sessionId, prevLogin, login);
- }
-
- byLogin
- .computeIfAbsent(login, id -> new CopyOnWriteArraySet<>())
- .add(ctx);
-
- log.debug("registered ctx (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Удалить подключение по контексту (например, при onClose).
- */
- public void remove(ConnectionContext ctx) {
- if (ctx == null) return;
-
- String sessionId = ctx.getSessionId();
- String login = ctx.getLogin();
-
- if (sessionId != null && !sessionId.isBlank()) {
- ConnectionContext removed = bySessionId.remove(sessionId);
-
- // Если в мапе лежал другой ctx под тем же sessionId — не трогаем его byLogin
- if (removed != null && removed != ctx) {
- log.debug("remove(ctx): sessionId mapped to another ctx, skip byLogin cleanup (sessionId={})", sessionId);
- return;
- }
- }
-
- if (login != null && !login.isBlank()) {
- Set set = byLogin.get(login);
- if (set != null) {
- set.remove(ctx);
- if (set.isEmpty()) {
- byLogin.remove(login);
- }
- }
- }
-
- log.debug("removed ctx (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Удалить подключение по sessionId.
- */
- public void removeBySessionId(String sessionId) {
- if (sessionId == null || sessionId.isBlank()) return;
-
- ConnectionContext ctx = bySessionId.remove(sessionId);
- if (ctx == null) return;
-
- String login = ctx.getLogin();
- if (login != null && !login.isBlank()) {
- Set set = byLogin.get(login);
- if (set != null) {
- set.remove(ctx);
- if (set.isEmpty()) {
- byLogin.remove(login);
- }
- }
- }
-
- log.debug("removed by sessionId (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Получить контекст по sessionId.
- */
- public ConnectionContext getBySessionId(String sessionId) {
- if (sessionId == null || sessionId.isBlank()) return null;
- return bySessionId.get(sessionId);
- }
-
- /**
- * Получить все активные подключения пользователя по login.
- */
- public Set getByLogin(String login) {
- if (login == null || login.isBlank()) return Set.of();
- Set set = byLogin.get(login);
- return (set == null) ? Set.of() : set; // CopyOnWriteArraySet можно отдавать как есть
- }
-}
-package server.logic.ws_protocol.JSON;
-
-import org.eclipse.jetty.websocket.api.Session;
-import shine.db.entities.SolanaUserEntry;
-import shine.db.entities.ActiveSessionEntry;
-
-/**
- * ConnectionContext — контекст состояния одного WebSocket-соединения.
- * Живёт ровно столько же, сколько живёт подключение.
- *
- * Важно (v2):
- * - Авторизация всегда 2 шага:
- * A) Создание новой сессии через deviceKey:
- * AuthChallenge(login) -> ctx.authNonce
- * CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
- *
- * B) Вход в существующую сессию через sessionKey:
- * SessionChallenge(sessionId) -> ctx.sessionLoginNonce + ctx.sessionLoginSessionId + expiresAt
- * SessionLogin(...) -> проверка подписи sessionKey по pubkey из БД -> ctx.AUTH_STATUS_USER
- */
-public class ConnectionContext {
-
- // Статусы аутентификации
- public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
- public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
- public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
-
- // Полный пользователь из БД (solana_users)
- private SolanaUserEntry solanaUserEntry;
-
- // Активная сессия из БД (active_sessions)
- private ActiveSessionEntry activeSessionEntry;
-
- /**
- * Идентификатор сессии — base64-строка от 32 байт.
- * Заполняется после успешного входа (AUTH_STATUS_USER).
- */
- private String sessionId;
-
- /**
- * Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
- * используется на шаге CreateAuthSession для проверки подписи deviceKey.
- */
- private String authNonce;
-
- /* ===================== SessionLogin challenge (v2) ===================== */
-
- /**
- * Одноразовый nonce, выданный на шаге SessionChallenge(sessionId),
- * используется на шаге SessionLogin для проверки подписи sessionKey.
- */
- private String sessionLoginNonce;
-
- /**
- * sessionId, для которого был выдан sessionLoginNonce.
- * Нужен, чтобы SessionLogin не мог "подставить" другой sessionId.
- */
- private String sessionLoginSessionId;
-
- /**
- * Время истечения sessionLoginNonce (мс с 1970-01-01).
- * Если текущее время > expiresAt, то nonce считается недействительным.
- */
- private long sessionLoginNonceExpiresAtMs;
-
- /* ====================================================================== */
-
- /**
- * Текущий статус аутентификации.
- * См. константы AUTH_STATUS_*
- */
- private int authenticationStatus = AUTH_STATUS_NONE;
-
- /**
- * WebSocket-сессия Jetty для данного подключения.
- * Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту.
- */
- private Session wsSession;
-
- // --- WebSocket Session ---
-
- public Session getWsSession() {
- return wsSession;
- }
-
- public void setWsSession(Session wsSession) {
- this.wsSession = wsSession;
- }
-
- // --- SolanaUser / ActiveSession ---
-
- public SolanaUserEntry getSolanaUser() {
- return solanaUserEntry;
- }
-
- public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
- this.solanaUserEntry = solanaUserEntry;
- }
-
- public ActiveSessionEntry getActiveSession() {
- return activeSessionEntry;
- }
-
- public void setActiveSession(ActiveSessionEntry activeSessionEntry) {
- this.activeSessionEntry = activeSessionEntry;
- }
-
- // --- Удобный геттер для логина ---
-
- public String getLogin() {
- return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
- }
-
- // --- sessionId ---
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- // --- authNonce ---
-
- public String getAuthNonce() {
- return authNonce;
- }
-
- public void setAuthNonce(String authNonce) {
- this.authNonce = authNonce;
- }
-
- // --- sessionLoginNonce (v2) ---
-
- public String getSessionLoginNonce() {
- return sessionLoginNonce;
- }
-
- public void setSessionLoginNonce(String sessionLoginNonce) {
- this.sessionLoginNonce = sessionLoginNonce;
- }
-
- public String getSessionLoginSessionId() {
- return sessionLoginSessionId;
- }
-
- public void setSessionLoginSessionId(String sessionLoginSessionId) {
- this.sessionLoginSessionId = sessionLoginSessionId;
- }
-
- public long getSessionLoginNonceExpiresAtMs() {
- return sessionLoginNonceExpiresAtMs;
- }
-
- public void setSessionLoginNonceExpiresAtMs(long sessionLoginNonceExpiresAtMs) {
- this.sessionLoginNonceExpiresAtMs = sessionLoginNonceExpiresAtMs;
- }
-
- // --- auth status ---
-
- public int getAuthenticationStatus() {
- return authenticationStatus;
- }
-
- public void setAuthenticationStatus(int authenticationStatus) {
- this.authenticationStatus = authenticationStatus;
- }
-
- public boolean isAuthenticatedUser() {
- return authenticationStatus == AUTH_STATUS_USER;
- }
-
- public boolean isAnonymous() {
- return authenticationStatus == AUTH_STATUS_NONE;
- }
-
- public void reset() {
- solanaUserEntry = null;
- activeSessionEntry = null;
-
- sessionId = null;
- authNonce = null;
-
- sessionLoginNonce = null;
- sessionLoginSessionId = null;
- sessionLoginNonceExpiresAtMs = 0;
-
- authenticationStatus = AUTH_STATUS_NONE;
- wsSession = null;
- }
-
- @Override
- public String toString() {
- return "ConnectionContext{" +
- "login='" + getLogin() + '\'' +
- ", sessionId=" + sessionId +
- ", authenticationStatus=" + authenticationStatus +
- '}';
- }
-}
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех событий (event).
- * Общие поля: op и payload.
- *.
- * Формат JSON (event):
- * {
- * "op": "...",
- * "payload": { ... }
- * }
- */
-public abstract class Net_Event {
-
- /** Имя операции / события (op). */
- private String op;
-
- /**
- * Произвольные данные.
- * В JSON это поле "payload".
- */
- private Object payload;
-
- // --- getters / setters ---
-
- public String getOp() {
- return op;
- }
-
- public void setOp(String op) {
- this.op = op;
- }
-
- public Object getPayload() {
- return payload;
- }
-
- public void setPayload(Object payload) {
- this.payload = payload;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Ответ с ошибкой (любой отказ).
- *.
- * В payload будет:
- * {
- * "code": "...",
- * "message": "..."
- * }
- */
-public class Net_Exception_Response extends Net_Response {
-
- 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;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех запросов (client → server).
- *.
- * Наследуется от NetEvent и добавляет requestId.
- *.
- * Формат JSON (request):
- * {
- * "op": "...",
- * "requestId": "...",
- * "payload": { ... }
- * }
- */
-public abstract class Net_Request extends Net_Event {
-
- /** Идентификатор запроса, чтобы связать запрос и ответ. */
- private String requestId;
-
- // --- getters / setters ---
-
- public String getRequestId() {
- return requestId;
- }
-
- public void setRequestId(String requestId) {
- this.requestId = requestId;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех ответов (server → client).
- *.
- * Наследуется от NetRequest и добавляет status.
- *.
- * Формат JSON (response):
- * {
- * "op": "...",
- * "requestId": "...",
- * "status": 200,
- * "payload": { ... } // и для успеха, и для ошибки
- * }
- */
-public abstract class Net_Response extends Net_Request {
-
- /** Статус результата (200 — успех, любое другое значение — ошибка). */
- private int status;
-
- // --- getters / setters ---
-
- public int getStatus() {
- return status;
- }
-
- public void setStatus(int status) {
- this.status = status;
- }
-
- public boolean isOk() {
- return status == 200;
- }
-}
-
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 авторизации: запрос выдачи одноразового nonce (authNonce).
- *
- * Клиент по логину просит сервер сгенерировать случайный authNonce,
- * который будет использован на втором шаге при подписи.
- *
- * Формат входящего JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "payload": {
- * "login": "someLogin"
- * }
- * }
- *
- * Формат успешного ответа:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Request extends Net_Request {
-
- /**
- * Логин пользователя, для которого запускается авторизация.
- */
- private String login;
-
- public String getLogin() {
- return login;
- }
- public void setLogin(String login) {
- this.login = login;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на AuthChallenge.
- *
- * При успехе сервер возвращает одноразовый nonce для подписи (authNonce),
- * который клиент обязан использовать на втором шаге при формировании строки
- * для цифровой подписи.
- *
- * JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Response extends Net_Response {
-
- /**
- * Одноразовый nonce для авторификации.
- * Строка — это base64-представление 32 случайных байт.
- */
- private String authNonce;
-
- public String getAuthNonce() {
- return authNonce;
- }
-
- public void setAuthNonce(String authNonce) {
- this.authNonce = authNonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос CloseActiveSession — закрытие активной сессии пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и "AUTH_IN_PROGRESS" здесь больше нет.
- *
- * payload:
- * {
- * "sessionId": "..." // опционально; если пусто — закрываем текущую
- * }
- */
-public class Net_CloseActiveSession_Request extends Net_Request {
-
- /** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CloseActiveSession.
- *
- * При успехе:
- * - status = 200;
- * - payload = {}.
- *
- * Закрытие WebSocket-соединения может быть выполнено сразу (для другой сессии)
- * или чуть позже (для текущей сессии) после отправки ответа.
- */
-public class Net_CloseActiveSession_Response extends Net_Response {
- // Дополнительных полей пока не требуется.
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
- *
- * Шаги:
- * 1) AuthChallenge(login) -> authNonce
- * 2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
- *
- * Подпись deviceKey делается над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
- *
- * Важно:
- * - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
- * - В БД active_sessions.session_key хранится sessionPubKeyB64.
- */
-public class Net_CreateAuthSession_Request extends Net_Request {
-
- /** Клиентский пароль для хранения данных (base64url от 32 байт). */
- private String storagePwd;
-
- /** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
- private String sessionPubKeyB64;
-
- /** Время на стороне клиента (мс с 1970-01-01). */
- private long timeMs;
-
- /** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-
- public String getSessionPubKeyB64() {
- return sessionPubKeyB64;
- }
-
- public void setSessionPubKeyB64(String sessionPubKeyB64) {
- this.sessionPubKeyB64 = sessionPubKeyB64;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CreateAuthSession (v2).
- *
- * При успехе сервер создаёт запись в active_sessions
- * и возвращает идентификатор сессии sessionId.
- *
- * JSON:
- * {
- * "op": "CreateAuthSession",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "sessionId": "base64url(32)"
- * }
- * }
- */
-public class Net_CreateAuthSession_Response extends Net_Response {
-
- /** Идентификатор сессии, base64url от 32 байт. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListSessions — список активных сессий пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Пустой payload.
- */
-public class Net_ListSessions_Request extends Net_Request {
- // пусто
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.List;
-
-/**
- * Ответ на ListSessions.
- *
- * При успехе:
- * - status = 200;
- * - payload:
- * {
- * "sessions": [
- * {
- * "sessionId": "...",
- * "clientInfoFromClient": "...",
- * "clientInfoFromRequest": "...",
- * "geo": "Country, City" | "unknown",
- * "lastAuthirificatedAtMs": 1733310000000
- * },
- * ...
- * ]
- * }
- */
-public class Net_ListSessions_Response extends Net_Response {
-
- /**
- * Список активных сессий для текущего пользователя.
- */
- private List sessions;
-
- public List getSessions() {
- return sessions;
- }
-
- public void setSessions(List sessions) {
- this.sessions = sessions;
- }
-
- /**
- * Описание одной активной сессии.
- */
- public static class SessionInfo {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- /** Что прислал клиент в CreateAuthSession/RefreshSession (clientInfo). */
- private String clientInfoFromClient;
-
- /** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
- private String clientInfoFromRequest;
-
- /** Строка геолокации вида "Country, City" или "unknown". */
- private String geo;
-
- /** Время последней успешной авторизации/refresh (мс с 1970-01-01). */
- private long lastAuthirificatedAtMs;
-
- // --- getters / setters ---
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- public String getClientInfoFromClient() {
- return clientInfoFromClient;
- }
-
- public void setClientInfoFromClient(String clientInfoFromClient) {
- this.clientInfoFromClient = clientInfoFromClient;
- }
-
- public String getClientInfoFromRequest() {
- return clientInfoFromRequest;
- }
-
- public void setClientInfoFromRequest(String clientInfoFromRequest) {
- this.clientInfoFromRequest = clientInfoFromRequest;
- }
-
- public String getGeo() {
- return geo;
- }
-
- public void setGeo(String geo) {
- this.geo = geo;
- }
-
- public long getLastAuthirificatedAtMs() {
- return lastAuthirificatedAtMs;
- }
-
- public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
- this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 входа в существующую сессию (v2):
- * SessionChallenge(sessionId) -> nonce
- */
-public class Net_SessionChallenge_Request extends Net_Request {
-
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionChallenge (v2).
- * payload: { "nonce": "base64url(32)" }
- */
-public class Net_SessionChallenge_Response extends Net_Response {
-
- private String nonce;
-
- public String getNonce() {
- return nonce;
- }
-
- public void setNonce(String nonce) {
- this.nonce = nonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 входа в существующую сессию (v2):
- * SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
- *
- * Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- *
- * nonce берётся из SessionChallenge и хранится в ctx (одноразовый, TTL).
- */
-public class Net_SessionLogin_Request extends Net_Request {
-
- private String sessionId;
- private long timeMs;
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionLogin (v2).
- * payload: { "storagePwd": "base64url(32)" }
- */
-public class Net_SessionLogin_Response extends Net_Response {
-
- private String storagePwd;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.security.SecureRandom;
-import java.util.Base64;
-
-/**
- * AuthChallenge (v2) — шаг 1 создания новой сессии.
- *
- * Логика авторизации (v2):
- * - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
- * - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
- * CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
- *
- * Что делает:
- * 1) Проверяет login.
- * 2) Находит пользователя (solana_users).
- * 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
- * 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
- */
-public class Net_AuthChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_AuthChallenge_Request req = (Net_AuthChallenge_Request) baseReq;
-
- String login = req.getLogin();
- if (login == null || login.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_LOGIN",
- "Пустой логин"
- );
- }
-
- // Если по этому соединению уже есть залогиненный пользователь — не даём повторную авторификацию
- if (ctx.getLogin() != null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "ALREADY_AUTHED",
- "Попытка повторной авторификации для уже заданного login=" + ctx.getLogin()
- );
- }
-
- SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
- if (solanaUserEntry == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "UNKNOWN_USER",
- "Пользователь с таким логином не найден"
- );
- }
-
- ctx.setSolanaUser(solanaUserEntry);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String authNonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
-
- ctx.setAuthNonce(authNonce);
-
- Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setAuthNonce(authNonce);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-/**
- * CloseActiveSession (v2) — закрытие текущей или другой сессии.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и AUTH_IN_PROGRESS здесь больше нет.
- *
- * Закрытие:
- * - удаляем запись из БД
- * - если по sessionId есть активный WS — закрываем его
- */
-public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- String targetSessionId = req.getSessionId();
- if (targetSessionId == null || targetSessionId.isBlank()) {
- if (ctx.getSessionId() != null && !ctx.getSessionId().isBlank()) {
- targetSessionId = ctx.getSessionId();
- } else if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
- targetSessionId = ctx.getActiveSession().getSessionId();
- } else {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_SESSION_TO_CLOSE",
- "Не удалось определить, какую сессию нужно закрыть"
- );
- }
- }
-
- ActiveSessionEntry targetSession;
- try {
- targetSession = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных при поиске сессии"
- );
- }
-
- if (targetSession == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия для закрытия не найдена"
- );
- }
-
- if (currentLogin == null || !currentLogin.equals(targetSession.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_OF_ANOTHER_USER",
- "Нельзя закрывать сессию другого пользователя"
- );
- }
-
- boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
-
- closeActiveSession(targetSessionId, ctx, isCurrentSession);
-
- Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- return resp;
- }
-
- private void closeActiveSession(String targetSessionId,
- ConnectionContext currentCtx,
- boolean isCurrentSession) {
-
- try {
- ActiveSessionsDAO.getInstance().deleteBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
- }
-
- ConnectionContext ctxToClose =
- ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
-
- if (ctxToClose == null) return;
-
- if (isCurrentSession && ctxToClose == currentCtx) {
- new Thread(() -> {
- try { Thread.sleep(50); } catch (InterruptedException ignored) {}
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }, "CloseSession-" + targetSessionId).start();
- } else {
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.eclipse.jetty.websocket.api.Session;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.sql.SQLException;
-import java.util.Base64;
-
-/**
- * CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
- *
- * Логика авторизации (v2):
- * - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
- * - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
- * отправляет на сервер ТОЛЬКО sessionPubKeyB64.
- * - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
- *
- * Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
- *
- * На выходе:
- * - создаётся запись active_sessions
- * - ctx становится AUTH_STATUS_USER (вход выполнен как "текущая сессия")
- * - ответ: sessionId
- */
-public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
- private static final SecureRandom RANDOM = new SecureRandom();
-
- public static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
-
- if (ctx == null
- || ctx.getSolanaUser() == null
- || ctx.getAuthNonce() == null
- || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
-
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_STEP1_CONTEXT",
- "Шаг 1 авторизации не был корректно выполнен для данного соединения"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no step1 context or bad auth state");
- return err;
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String login = user.getLogin();
- if (login == null || login.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_LOGIN",
- "Для пользователя не задан login в БД"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no login");
- return err;
- }
-
- String storagePwd = req.getStoragePwd();
- if (storagePwd == null || storagePwd.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_STORAGE_PWD",
- "Пустой storagePwd"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty storagePwd");
- return err;
- }
-
- String sessionPubKeyB64 = req.getSessionPubKeyB64();
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_PUBKEY",
- "Пустой sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
- return err;
- }
-
- // Проверим, что sessionPubKeyB64 декодируется в 32 байта
- byte[] sessionPubKey32;
- try {
- sessionPubKey32 = decodeBase64Any(sessionPubKeyB64);
- } catch (IllegalArgumentException e) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный base64 в sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
- return err;
- }
- if (sessionPubKey32.length != 32) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_SESSION_PUBKEY_LEN",
- "sessionPubKey должен быть 32 байта"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
- return err;
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая цифровая подпись"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty signature");
- return err;
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- long diff = Math.abs(nowMs - timeMs);
- if (diff > ALLOWED_SKEW_MS) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: time skew");
- return err;
- }
-
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String devicePubKeyB64 = user.getDeviceKey();
- if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_DEVICE_KEY",
- "Отсутствует deviceKey у пользователя"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no deviceKey");
- return err;
- }
-
- String authNonce = ctx.getAuthNonce();
-
- boolean sigOk;
- try {
- sigOk = verifyCreateSessionSignature(
- user,
- login,
- authNonce,
- timeMs,
- signatureB64
- );
- } catch (IllegalArgumentException ex) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный формат Base64 для ключа или подписи"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad base64");
- return err;
- }
-
- if (!sigOk) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad signature");
- return err;
- }
-
- // --- генерируем sessionId ---
- String sessionId = generateRandom32B64Url();
- long now = System.currentTimeMillis();
-
- // --- Сбор данных о клиенте (IP, UA, язык) ---
- Session wsSession = ctx.getWsSession();
- String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
- String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
-
- String clientIp = "";
- if (wsSession != null) {
- String ip = ClientInfoService.extractClientIp(wsSession);
- if (ip != null) clientIp = ip;
-
- if (!clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- // --- создаём запись ActiveSession и сохраняем в БД ---
- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
- ActiveSessionEntry activeSessionEntry;
-
- try {
- activeSessionEntry = new ActiveSessionEntry(
- sessionId,
- login,
- sessionPubKeyB64, // session_key (pubkey)
- storagePwd,
- now,
- now,
- null, // pushEndpoint
- null, // pushP256dhKey
- null, // pushAuthKey
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
-
- dao.insert(activeSessionEntry);
- } catch (SQLException e) {
- log.error("Ошибка БД при создании новой сессии для login={}", login, e);
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_SESSION_CREATE",
- "Ошибка БД при создании сессии"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db error");
- return err;
- }
-
- // --- обновляем контекст ---
- ctx.setActiveSession(activeSessionEntry);
- ctx.setSessionId(sessionId);
- ctx.setAuthNonce(null);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // --- формируем ответ ---
- Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessionId(sessionId);
- return resp;
- }
-
- private static boolean verifyCreateSessionSignature(
- SolanaUserEntry user,
- String login,
- String authNonce,
- long timeMs,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // deviceKey (pub, 32)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
- byte[] signature64 = decodeBase64Any(signatureB64);
-
- String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-
- private static String generateRandom32B64Url() {
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
- }
-
- private static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
- if (s == null) throw new IllegalArgumentException("base64 is null");
- String x = s.trim();
- if (x.isEmpty()) throw new IllegalArgumentException("base64 is empty");
-
- // сначала url-safe, потом обычный
- try {
- return Base64.getUrlDecoder().decode(x);
- } catch (IllegalArgumentException ignore) {
- return Base64.getDecoder().decode(x);
- }
- }
-}
-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.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.GeoLookupService;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListSessions (v2) — список активных сессий.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей здесь больше нет.
- */
-public class Net_ListSessions_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListSessions_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- List sessions;
- try {
- sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
- } catch (SQLException e) {
- log.error("Ошибка БД при получении списка сессий для login={}", currentLogin, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_LIST_SESSIONS",
- "Ошибка доступа к базе данных при получении списка сессий"
- );
- }
-
- List resultList = new ArrayList<>();
- for (ActiveSessionEntry s : sessions) {
- SessionInfo info = new SessionInfo();
- info.setSessionId(s.getSessionId());
- info.setClientInfoFromClient(s.getClientInfoFromClient());
- info.setClientInfoFromRequest(s.getClientInfoFromRequest());
- info.setLastAuthirificatedAtMs(s.getLastAuthirificatedAtMs());
-
- String ip = s.getClientIp();
- String geo = GeoLookupService.resolveCountryCityOrIpWithCache(ip);
- info.setGeo(geo);
-
- resultList.add(info);
- }
-
- Net_ListSessions_Response resp = new Net_ListSessions_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessions(resultList);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-
-import java.security.SecureRandom;
-import java.sql.SQLException;
-import java.util.Base64;
-
-/**
- * SessionChallenge (v2) — шаг 1 входа в существующую сессию.
- *
- * Логика авторизации (v2):
- * - Вход в существующую сессию ВСЕГДА в 2 шага:
- * 1) SessionChallenge(sessionId) -> nonce
- * 2) SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
- *
- * Что делает:
- * - Проверяет, что sessionId существует в БД.
- * - Генерирует одноразовый nonce (base64url(32)), сохраняет его в ctx:
- * ctx.sessionLoginNonce, ctx.sessionLoginSessionId, ctx.sessionLoginNonceExpiresAtMs.
- */
-public class Net_SessionChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
- private static final long NONCE_TTL_MS = 60_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionChallenge_Request req = (Net_SessionChallenge_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String nonce = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
-
- long now = System.currentTimeMillis();
- ctx.setSessionLoginNonce(nonce);
- ctx.setSessionLoginSessionId(sessionId);
- ctx.setSessionLoginNonceExpiresAtMs(now + NONCE_TTL_MS);
-
- Net_SessionChallenge_Response resp = new Net_SessionChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setNonce(nonce);
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.SQLException;
-import java.util.Base64;
-
-/**
- * SessionLogin (v2) — шаг 2 входа в существующую сессию (по sessionKey).
- *
- * Логика авторизации (v2):
- * - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
- * - SessionLogin проверяет подпись sessionKey над строкой:
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- * - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
- *
- * При успехе:
- * - ctx становится AUTH_STATUS_USER
- * - обновляем метаданные сессии (lastAuth + clientIp + clientInfo + lang)
- * - возвращаем storagePwd
- */
-public class Net_SessionLogin_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
-
- private static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- // проверка челленджа
- if (ctx.getSessionLoginNonce() == null
- || ctx.getSessionLoginSessionId() == null
- || System.currentTimeMillis() > ctx.getSessionLoginNonceExpiresAtMs()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_CHALLENGE",
- "Нет активного SessionChallenge или nonce истёк"
- );
- }
-
- if (!sessionId.equals(ctx.getSessionLoginSessionId())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "SESSION_ID_MISMATCH",
- "nonce был выдан для другого sessionId"
- );
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая подпись"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- String sessionPubKeyB64 = session.getSessionKey(); // это pubKey
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_SESSION_KEY",
- "В сессии не задан session_key"
- );
- }
-
- String nonce = ctx.getSessionLoginNonce();
-
- boolean sigOk;
- try {
- sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный Base64 для ключа/подписи"
- );
- }
-
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- }
-
- // сжигаем nonce
- ctx.setSessionLoginNonce(null);
- ctx.setSessionLoginSessionId(null);
- ctx.setSessionLoginNonceExpiresAtMs(0);
-
- // подтягиваем пользователя
- SolanaUserEntry user;
- try {
- user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_USER_LOOKUP",
- "Ошибка доступа к базе данных при получении пользователя"
- );
- }
-
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "USER_NOT_FOUND_FOR_SESSION",
- "Пользователь для данной сессии не найден"
- );
- }
-
- // обновление метаданных
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String clientIp = null;
- String clientInfoFromRequest = null;
- String userLanguage = null;
-
- if (ctx.getWsSession() != null) {
- clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
- clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
- userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
-
- if (clientIp != null && !clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- long now = System.currentTimeMillis();
- try {
- ActiveSessionsDAO.getInstance().updateOnRefresh(
- sessionId,
- now,
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
- } catch (SQLException e) {
- log.error("Ошибка БД при updateOnRefresh sessionId={}", sessionId, e);
- }
-
- session.setLastAuthirificatedAtMs(now);
- session.setClientIp(clientIp);
- session.setClientInfoFromClient(clientInfoFromClient);
- session.setClientInfoFromRequest(clientInfoFromRequest);
- session.setUserLanguage(userLanguage);
-
- // ctx
- ctx.setActiveSession(session);
- ctx.setSolanaUser(user);
- ctx.setSessionId(sessionId);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // ответ
- Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setStoragePwd(session.getStoragePwd());
- return resp;
- }
-
- private static boolean verifySessionLoginSignature(
- String sessionPubKeyB64,
- String sessionId,
- long timeMs,
- String nonce,
- String signatureB64
- ) throws IllegalArgumentException {
-
- byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);
- byte[] signature64 = decodeBase64Any(signatureB64);
-
- String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-
- private static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
- try {
- return Base64.getUrlDecoder().decode(s);
- } catch (IllegalArgumentException ignore) {
- return Base64.getDecoder().decode(s);
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-public final class Net_AddBlock_Request extends Net_Request {
-
- private String blockchainName; // обязателен
- private int blockNumber; // обязателен
- private String prevBlockHash; // HEX(64) или "" для нулевого
- private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public int getBlockNumber() { return blockNumber; }
- public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; }
-
- public String getPrevBlockHash() { return prevBlockHash; }
- public void setPrevBlockHash(String prevBlockHash) { this.prevBlockHash = prevBlockHash; }
-
- public String getBlockBytesB64() { return blockBytesB64; }
- public void setBlockBytesB64(String blockBytesB64) { this.blockBytesB64 = blockBytesB64; }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ:
- * - reasonCode (null если ok)
- * - serverLastGlobalNumber / serverLastGlobalHash
- */
-public final class Net_AddBlock_Response extends Net_Response {
-
- /** null если ok, иначе строка причины (bad_block_base64, user_not_found, и т.п.) */
- private String reasonCode;
-
- /** что сервер считает последним по глобальной цепочке */
- private int serverLastGlobalNumber;
- private String serverLastGlobalHash;
-
- public String getReasonCode() { return reasonCode; }
- public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
-
- 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; }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain;
-
-import blockchain.BchBlockEntry;
-import blockchain.BchCryptoVerifier;
-import blockchain.MsgSubType;
-import blockchain.body.BodyHasLine;
-import blockchain.body.BodyHasTarget;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainLocks;
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.BlocksDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.BlockEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.util.Arrays;
-import java.util.Base64;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * Net_AddBlock_Handler — единый хэндлер добавления блока (JSON).
- *
- * Новый порядок валидации (ТЗ):
- * 1) Достаём из blockchain_state: last_block_number, last_block_hash
- * 2) Проверяем:
- * - incoming.blockNumber == last+1
- * - incoming.prevHash32 == last_hash (для genesis last_hash = 32 нулей)
- * 3) Проверяем подпись Ed25519.verify(hash32(preimage), signature64, pubKey)
- * 4) Если тип имеет линию:
- * - если prevLineNumber != null:
- * достаём hash блока prevLineNumber из blocks
- * сравниваем с prevLineHash32 из body
- * 5) Сохраняем блок в blocks + обновляем blockchain_state
- *
- * Важно:
- * - Сетевой протокол AddBlock пока оставляем старые поля (globalNumber/prevGlobalHash),
- * но внутренняя логика использует НОВЫЙ формат блока.
- */
-public final class Net_AddBlock_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_Handler.class);
-
- private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
- private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) {
-
- Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;
-
- String blockchainName = req.getBlockchainName();
- ReentrantLock lock = BlockchainLocks.lockFor(blockchainName);
- lock.lock();
- try {
- AddBlockResult r = addBlock(
- blockchainName,
- req.getBlockNumber(), // старое поле, пока оставляем
- req.getPrevBlockHash(), // старое поле, пока оставляем
- req.getBlockBytesB64()
- );
-
- Net_AddBlock_Response resp = new Net_AddBlock_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
-
- if (r.isOk()) {
- resp.setStatus(WireCodes.Status.OK);
- resp.setReasonCode(null);
- } else {
- resp.setStatus(r.httpStatus);
- resp.setReasonCode(r.reasonCode);
- }
-
- resp.setServerLastGlobalNumber(r.serverLastBlockNumber);
- resp.setServerLastGlobalHash(r.serverLastBlockHashHex);
-
- return resp;
-
- } finally {
- lock.unlock();
- }
- }
-
- private AddBlockResult addBlock(
- String blockchainName,
- int globalNumberFromReq,
- String prevGlobalHashHexFromReq,
- String blockBytesB64
- ) {
- if (blockchainName == null || blockchainName.isBlank()) {
- log.warn("AddBlock: пустой blockchainName (reqGlobalNumber={})", globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
- }
-
- String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
- if (login == null || login.isBlank()) {
- log.warn("AddBlock: плохой blockchainName='{}' => login не получился (reqGlobalNumber={})",
- blockchainName, globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
- }
-
- // 1) state обязателен
- final BlockchainStateEntry st;
- try {
- st = stateDAO.getByBlockchainName(blockchainName);
- } catch (Exception e) {
- log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
- }
-
- if (st == null) {
- log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
- }
-
- final int serverLastNum = st.getLastBlockNumber();
- final byte[] serverLastHash32 = (serverLastNum < 0)
- ? new byte[32]
- : require32OrThrow(st.getLastBlockHash(), "state.last_block_hash is null/invalid");
-
- final String serverLastHashHex = toHex(serverLastHash32);
-
- // 2) decode block
- final byte[] blockBytes;
- try {
- blockBytes = decodeBase64(blockBytesB64);
- } catch (Exception e) {
- log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
- }
-
- // 3) лимит (оставляем как было)
- try {
- long oldSize = st.getFileSizeBytes();
- long limit = st.getSizeLimit();
- long newSize = safeAdd(oldSize, blockBytes.length);
-
- if (limit > 0 && newSize > limit) {
- log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, oldSize={}, addLen={}, newSize={}, limit={})",
- login, blockchainName, oldSize, blockBytes.length, newSize, limit);
- return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
- }
- } catch (Exception e) {
- log.error("AddBlock: limit_check_failed (login={}, blockchainName={})", login, blockchainName, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
- }
-
- // 4) parse block
- final BchBlockEntry block;
- try {
- block = new BchBlockEntry(blockBytes);
- } catch (Exception e) {
- log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, bytesLen={})",
- login, blockchainName, blockBytes.length, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
- }
-
- // body.check()
- try {
- block.body.check();
- } catch (Exception e) {
- log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, blockNumber={}, type={}, ver={})",
- login, blockchainName, block.blockNumber, (block.type & 0xFFFF), (block.version & 0xFFFF), e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
- }
-
- // 4.2) запрет дырок: blockNumber строго last+1
- int expectedBlockNumber = serverLastNum + 1;
- if (block.blockNumber != expectedBlockNumber) {
- log.warn("AddBlock: bad_block_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={})",
- login, blockchainName, block.blockNumber, expectedBlockNumber, serverLastNum);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_number", serverLastNum, serverLastHashHex);
- }
-
- // (временная совместимость) req.globalNumber должен совпасть с block.blockNumber
- if (globalNumberFromReq != block.blockNumber) {
- log.warn("AddBlock: req_global_mismatch (login={}, blockchainName={}, reqGlobal={}, blockNumber={})",
- login, blockchainName, globalNumberFromReq, block.blockNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "req_global_mismatch", serverLastNum, serverLastHashHex);
- }
-
- // 4.3) проверка цепочки по prevHash32
- if (!Arrays.equals(block.prevHash32, serverLastHash32)) {
- log.warn("AddBlock: bad_prev_hash (login={}, blockchainName={}, blockNumber={}, clientPrev={}, serverPrev={})",
- login, blockchainName, block.blockNumber, toHex(block.prevHash32), serverLastHashHex);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_hash", serverLastNum, serverLastHashHex);
- }
-
- // 5) pubKey
- final byte[] pubKey32 = st.getBlockchainKeyBytes();
- if (pubKey32 == null || pubKey32.length != 32) {
- log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, blockNumber={}, keyLen={})",
- login, blockchainName, block.blockNumber, (pubKey32 == null ? -1 : pubKey32.length));
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
- }
-
- // 6) подпись по hash32(preimage)
- boolean sigOk;
- try {
- sigOk = BchCryptoVerifier.verifyBlock(block, pubKey32);
- } catch (Exception e) {
- log.warn("AddBlock: signature_verify_failed (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
- }
-
- if (!sigOk) {
- log.warn("AddBlock: bad_signature (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
- }
-
- // 7) line columns (only for BodyHasLine)
- Integer lineCode = null;
- Integer prevLineNumber = null;
- byte[] prevLineHash32 = null;
- Integer thisLineNumber = null;
-
- if (block.body instanceof BodyHasLine bl) {
- lineCode = bl.lineCode();
- prevLineNumber = bl.prevLineBlockGlobalNumber();
- prevLineHash32 = bl.prevLineBlockHash32();
- thisLineNumber = bl.lineSeq();
-
- // Нормализация: -1 не пишем в БД (для совместимости со старым TextBody)
- if (prevLineNumber != null && prevLineNumber == -1) {
- prevLineNumber = null;
- prevLineHash32 = null;
- thisLineNumber = null;
- }
-
- // Если prevLineNumber задан — проверяем его хэш
- if (prevLineNumber != null) {
- try {
- byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
- if (dbPrevHash == null) {
- log.warn("AddBlock: prev_line_block_not_found (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
- login, blockchainName, block.blockNumber, prevLineNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_line_block_not_found", serverLastNum, serverLastHashHex);
- }
- if (!Arrays.equals(dbPrevHash, require32OrThrow(prevLineHash32, "prevLineHash32 invalid"))) {
- log.warn("AddBlock: bad_prev_line_hash (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
- login, blockchainName, block.blockNumber, prevLineNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_line_hash", serverLastNum, serverLastHashHex);
- }
- } catch (Exception e) {
- log.error("AddBlock: db_error_prev_line_check (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_prev_line_check", serverLastNum, serverLastHashHex);
- }
- }
- }
-
- // 8) сформировать запись и записать (DB + state + файл)
- try {
- BlockEntry be = new BlockEntry();
- be.setLogin(login);
- be.setBchName(blockchainName);
-
- be.setBlockNumber(block.blockNumber);
- be.setMsgType(block.type & 0xFFFF);
- be.setMsgSubType(block.subType & 0xFFFF);
-
- be.setBlockBytes(block.toBytes());
- be.setBlockHash(block.getHash32());
- be.setBlockSignature(block.getSignature64());
-
- // line columns (optional)
- be.setLineCode(lineCode);
- be.setPrevLineNumber(prevLineNumber);
- be.setPrevLineHash(prevLineHash32);
- be.setThisLineNumber(thisLineNumber);
-
- // target columns (optional)
- if (block.body instanceof BodyHasTarget t) {
- be.setToLogin(t.toLogin());
- be.setToBchName(t.toBchName());
- be.setToBlockNumber(t.toBlockGlobalNumber());
- be.setToBlockHash(t.toBlockHashBytes());
- }
-
- // edit helper (optional): если TEXT_EDIT_* — это "редактирование блока цели"
- int type = block.type & 0xFFFF;
- int sub = block.subType & 0xFFFF;
-
- if (type == 1
- && (sub == (MsgSubType.TEXT_EDIT_POST & 0xFFFF) || sub == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF))
- && be.getToBlockNumber() != null) {
- be.setEditedByBlockNumber(be.getToBlockNumber());
- }
-
- dbWriter.appendBlockAndState(blockchainName, block, st, be);
-
- } catch (Exception e) {
- log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
- }
-
- String newHashHex = toHex(block.getHash32());
-
- log.info("✅ AddBlock ok: login={}, blockchainName={}, blockNumber={}, newHash={}",
- login, blockchainName, block.blockNumber, newHashHex);
-
- return new AddBlockResult(WireCodes.Status.OK, null, block.blockNumber, newHashHex);
- }
-
- /* ===================================================================== */
- /* ====================== Helpers ====================================== */
- /* ===================================================================== */
-
- private static byte[] decodeBase64(String b64) {
- if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
- return Base64.getDecoder().decode(b64);
- }
-
- private static long safeAdd(long a, long b) {
- long r = a + b;
- if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
- return r;
- }
-
- private static byte[] require32OrThrow(byte[] b, String msg) {
- if (b == null || b.length != 32) throw new IllegalArgumentException(msg);
- return b;
- }
-
- private static String toHex(byte[] bytes) {
- if (bytes == null) return "null";
- char[] HEX = "0123456789abcdef".toCharArray();
- char[] out = new char[bytes.length * 2];
- for (int i = 0; i < bytes.length; i++) {
- int v = bytes[i] & 0xFF;
- out[i * 2] = HEX[v >>> 4];
- out[i * 2 + 1] = HEX[v & 0x0F];
- }
- return new String(out);
- }
-
- private static final class AddBlockResult {
- final int httpStatus;
- final String reasonCode;
- final int serverLastBlockNumber;
- final String serverLastBlockHashHex;
-
- AddBlockResult(int httpStatus, String reasonCode, int serverLastBlockNumber, String serverLastBlockHashHex) {
- this.httpStatus = httpStatus;
- this.reasonCode = reasonCode;
- this.serverLastBlockNumber = serverLastBlockNumber;
- this.serverLastBlockHashHex = serverLastBlockHashHex;
- }
-
- boolean isOk() { return httpStatus == WireCodes.Status.OK; }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.ReentrantLock;
-
-public final class BlockchainLocks {
- private static final ConcurrentHashMap MAP = new ConcurrentHashMap<>();
-
- private BlockchainLocks() {}
-
- public static ReentrantLock lockFor(String blockchainName) {
- return MAP.computeIfAbsent(blockchainName, id -> new ReentrantLock(true)); // fair=true
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
-
-import blockchain.BchBlockEntry;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.BlocksDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.BlockEntry;
-import utils.files.FileStoreUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-/**
- * BlockchainWriter — запись блока в DB + обновление state + запись в файл.
- *
- * ВАЖНО:
- * - Это минимальный рабочий вариант под новый формат.
- * - Если у тебя уже есть "атомарность" сложнее (tmp_bch + commit/recovery) — можно усилить потом.
- */
-public final class BlockchainWriter {
-
- private final BlocksDAO blocksDAO;
- private final BlockchainStateDAO stateDAO;
- private final FileStoreUtil fs = FileStoreUtil.getInstance();
-
- public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
- this.blocksDAO = blocksDAO;
- this.stateDAO = stateDAO;
- }
-
- public void appendBlockAndState(String blockchainName,
- BchBlockEntry block,
- BlockchainStateEntry st,
- BlockEntry be) throws SQLException {
-
- long nowMs = System.currentTimeMillis();
-
- try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
- c.setAutoCommit(false);
- try {
- // 1) insert block
- blocksDAO.insert(c, be);
-
- // 2) update state
- st.setLastBlockNumber(block.blockNumber);
- st.setLastBlockHash(block.getHash32());
- st.setFileSizeBytes(st.getFileSizeBytes() + block.toBytes().length);
- st.setUpdatedAtMs(nowMs);
-
- stateDAO.upsert(c, st);
-
- c.commit();
- } catch (Exception e) {
- try { c.rollback(); } catch (Exception ignored) {}
- if (e instanceof SQLException se) throw se;
- throw new SQLException("appendBlockAndState failed", e);
- } finally {
- try { c.setAutoCommit(true); } catch (Exception ignored) {}
- }
- }
-
- // 3) append to file (минимально: просто дописать)
- // Если у тебя уже есть логика tmp_bch+atomicReplace — можно заменить тут.
- String fileName = fs.buildBlockchainFileName(blockchainName);
- fs.addDataToFile(fileName, block.toBytes());
- }
-}
-package server.logic.ws_protocol.JSON.handlers;
-
-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;
-
-/**
- * Общий интерфейс для всех JSON-хэндлеров.
- */
-public interface JsonMessageHandler {
-
- /**
- * Обработать запрос и вернуть ответ.
- *
- * @param request распарсенный запрос
- * @param ctx контекст текущего WebSocket-соединения
- */
- Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception;
-}
-
-package server.logic.ws_protocol.JSON.handlers.subscriptions.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetSubscribedChannels.
- *
- * Клиент отправляет:
- * {
- * "op": "GetSubscribedChannels",
- * "requestId": "....",
- * "payload": {
- * "login": "anya"
- * }
- * }
- */
-public class Net_GetSubscribedChannels_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.subscriptions.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.List;
-
-/**
- * Ответ GetSubscribedChannels.
- *
- * payload:
- * {
- * "channels": [
- * {
- * "channelLogin": "dima",
- * "channelBchName": "dima-001",
- * "publicationsCount": 123,
- * "lastPublicationTimestampSec": 1736371200,
- * "lastTextPreview": "...."
- * }
- * ]
- * }
- */
-public class Net_GetSubscribedChannels_Response extends Net_Response {
-
- private List channels;
-
- public List getChannels() { return channels; }
- public void setChannels(List channels) { this.channels = channels; }
-
- public static class ChannelInfo {
-
- private String channelLogin;
- private String channelBchName;
-
- private Integer publicationsCount;
-
- /** Unix seconds времени ПУБЛИКАЦИИ (оригинального TEXT_NEW). Nullable, если публикаций нет. */
- private Long lastPublicationTimestampSec;
-
- /** Первые 50 символов актуального текста (edit или orig). Nullable, если публикаций нет. */
- private String lastTextPreview;
-
- public String getChannelLogin() { return channelLogin; }
- public void setChannelLogin(String channelLogin) { this.channelLogin = channelLogin; }
-
- public String getChannelBchName() { return channelBchName; }
- public void setChannelBchName(String channelBchName) { this.channelBchName = channelBchName; }
-
- public Integer getPublicationsCount() { return publicationsCount; }
- public void setPublicationsCount(Integer publicationsCount) { this.publicationsCount = publicationsCount; }
-
- public Long getLastPublicationTimestampSec() { return lastPublicationTimestampSec; }
- public void setLastPublicationTimestampSec(Long lastPublicationTimestampSec) { this.lastPublicationTimestampSec = lastPublicationTimestampSec; }
-
- public String getLastTextPreview() { return lastTextPreview; }
- public void setLastTextPreview(String lastTextPreview) { this.lastTextPreview = lastTextPreview; }
- }
-}
-//package server.logic.ws_protocol.JSON.handlers.subscriptions;
-//
-//import blockchain.BchBlockEntry;
-//import blockchain.body.TextBody;
-//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.handlers.JsonMessageHandler;
-//import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
-//import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Response;
-//import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-//import server.logic.ws_protocol.WireCodes;
-//import shine.db.SqliteDbController;
-//import shine.db.dao.SubscriptionsDAO;
-//
-//import java.sql.Connection;
-//import java.sql.SQLException;
-//import java.util.ArrayList;
-//import java.util.List;
-//
-///**
-// * Handler: GetSubscribedChannels
-// *
-// * Логика:
-// * - DAO возвращает last publication orig bytes (+ edit bytes если есть)
-// * - Handler парсит FULL bytes блока:
-// * timestamp берём из ОРИГИНАЛА (publication)
-// * текст берём из EDIT (если есть) иначе из оригинала
-// * - формируем превью первых 50 символов
-// */
-//public class Net_GetSubscribedChannels_Handler implements JsonMessageHandler {
-//
-// private static final Logger log = LoggerFactory.getLogger(Net_GetSubscribedChannels_Handler.class);
-//
-// @Override
-// public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
-// Net_GetSubscribedChannels_Request req = (Net_GetSubscribedChannels_Request) baseRequest;
-//
-// if (req.getLogin() == null || req.getLogin().isBlank()) {
-// return NetExceptionResponseFactory.error(
-// req,
-// WireCodes.Status.BAD_REQUEST,
-// "BAD_FIELDS",
-// "Некорректное поле: login"
-// );
-// }
-//
-// // Если хочешь жёстче:
-// // if (!req.getLogin().matches("^[A-Za-z0-9_]+$")) ...
-//
-// SubscriptionsDAO dao = SubscriptionsDAO.getInstance();
-// SqliteDbController db = SqliteDbController.getInstance();
-//
-// try (Connection c = db.getConnection()) {
-//
-// List rows = dao.getSubscribedChannels(c, req.getLogin());
-// List out = new ArrayList<>(rows.size());
-//
-// for (SubscriptionsDAO.ChannelRow r : rows) {
-// Net_GetSubscribedChannels_Response.ChannelInfo dto =
-// new Net_GetSubscribedChannels_Response.ChannelInfo();
-//
-// dto.setChannelLogin(r.getChannelLogin());
-// dto.setChannelBchName(r.getChannelBchName());
-// dto.setPublicationsCount(r.getPublicationsCount());
-//
-// byte[] pubBytes = r.getLastPublicationBlockBytes();
-// byte[] editBytes = r.getLastEditBlockBytes();
-//
-// if (pubBytes == null || pubBytes.length == 0) {
-// dto.setLastPublicationTimestampSec(null);
-// dto.setLastTextPreview(null);
-// out.add(dto);
-// continue;
-// }
-//
-// // 1) timestamp берём из ОРИГИНАЛЬНОЙ публикации
-// BchBlockEntry pubBlock = new BchBlockEntry(pubBytes);
-// dto.setLastPublicationTimestampSec(pubBlock.timestamp);
-//
-// // 2) текст — из EDIT (если есть) иначе из оригинала
-// byte[] actualBytes = (editBytes != null && editBytes.length > 0) ? editBytes : pubBytes;
-// BchBlockEntry actualBlock = new BchBlockEntry(actualBytes);
-//
-// if (!(actualBlock.body instanceof TextBody)) {
-// // Это уже нарушение данных: last publication должен быть текстовым блоком.
-// throw new IllegalStateException("Last publication is not TextBody: type="
-// + (actualBlock.body == null ? "null" : (actualBlock.body.type() & 0xFFFF)));
-// }
-//
-// String msg = ((TextBody) actualBlock.body).message;
-// dto.setLastTextPreview(firstNCharsSafe(msg, 50));
-//
-// out.add(dto);
-// }
-//
-// Net_GetSubscribedChannels_Response resp = new Net_GetSubscribedChannels_Response();
-// resp.setOp(req.getOp());
-// resp.setRequestId(req.getRequestId());
-// resp.setStatus(WireCodes.Status.OK);
-// resp.setChannels(out);
-//
-// return resp;
-//
-// } catch (SQLException e) {
-// log.error("❌ DB error GetSubscribedChannels", e);
-// return NetExceptionResponseFactory.error(
-// req,
-// WireCodes.Status.SERVER_DATA_ERROR,
-// "DB_ERROR",
-// "Ошибка БД"
-// );
-// } catch (IllegalArgumentException e) {
-// // сюда попадёт, например, если BchBlockEntry не смог распарсить block_byte
-// log.error("❌ Bad block bytes in DB (cannot parse BchBlockEntry)", e);
-// return NetExceptionResponseFactory.error(
-// req,
-// WireCodes.Status.SERVER_DATA_ERROR,
-// "BAD_BLOCK_BYTES",
-// "В БД обнаружен повреждённый блок"
-// );
-// } catch (Exception e) {
-// log.error("❌ Internal error GetSubscribedChannels", e);
-// return NetExceptionResponseFactory.error(
-// req,
-// WireCodes.Status.INTERNAL_ERROR,
-// "INTERNAL_ERROR",
-// "Внутренняя ошибка сервера"
-// );
-// }
-// }
-//
-// /**
-// * Берём первые N "символов" безопасно для emoji/суррогатных пар:
-// * режем по code points.
-// */
-// private static String firstNCharsSafe(String s, int n) {
-// if (s == null) return null;
-// if (n <= 0) return "";
-// int cp = s.codePointCount(0, s.length());
-// if (cp <= n) return s;
-// int end = s.offsetByCodePoints(0, n);
-// return s.substring(0, end);
-// }
-//}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос AddUser — временная/тестовая регистрация локального пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "payload": {
- * "login": "anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "base64-ed25519-public-key-login",
- * "blockchainKey": "base64-ed25519-public-key-blockchain",
- * "deviceKey": "base64-ed25519-public-key-device",
- * "bchLimit": 1000000
- * }
- * }
- *
- * Все поля лежат внутри payload.
- */
-public class Net_AddUser_Request extends Net_Request {
-
- private String login;
- private String blockchainName;
-
- /** Ключ пользователя Solana (публичный ключ логина) */
- private String solanaKey;
-
- /** Ключ блокчейна (публичный ключ блокчейна) */
- private String blockchainKey;
-
- /** Ключ устройства (публичный ключ устройства) */
- private String deviceKey;
-
- private Integer bchLimit;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-
- public Integer getBchLimit() { return bchLimit; }
- public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
-}
-// file: server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Успешный ответ на AddUser.
- *
- * Сейчас дополнительных полей нет — достаточно status=200.
- *
- * Пример:
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_AddUser_Response extends Net_Response {
- // При необходимости сюда можно добавить, например, флаг created/updated и т.п.
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUser — проверка/получение пользователя по login.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "payload": {
- * "login": "AnYa"
- * }
- * }
- *
- * Поиск по login выполняется без учёта регистра.
- * В ответе возвращаем login/blockchainName с тем регистром, как в БД.
- */
-public class Net_GetUser_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUser.
- *
- * Всегда status=200.
- *
- * Пример (нет пользователя):
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "status": 200,
- * "payload": { "exists": false }
- * }
- *
- * Пример (есть пользователь):
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "status": 200,
- * "payload": {
- * "exists": true,
- * "login": "Anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "...",
- * "blockchainKey": "...",
- * "deviceKey": "..."
- * }
- * }
- */
-public class Net_GetUser_Response extends Net_Response {
-
- private Boolean exists;
-
- private String login;
- private String blockchainName;
- private String solanaKey;
- private String blockchainKey;
- private String deviceKey;
-
- public Boolean getExists() { return exists; }
- public void setExists(Boolean exists) { this.exists = exists; }
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос SearchUsers — поиск логинов по префиксу.
- *
- * Клиент отправляет:
- * {
- * "op": "SearchUsers",
- * "requestId": "su-1",
- * "payload": { "prefix": "any" }
- * }
- *
- * Поиск по prefix выполняется без учёта регистра.
- * В ответе возвращаем логины с тем регистром, как в БД.
- */
-public class Net_SearchUsers_Request extends Net_Request {
-
- private String prefix;
-
- public String getPrefix() { return prefix; }
- public void setPrefix(String prefix) { this.prefix = prefix; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ SearchUsers.
- *
- * Всегда status=200.
- *
- * Пример:
- * {
- * "op": "SearchUsers",
- * "requestId": "su-1",
- * "status": 200,
- * "payload": {
- * "logins": ["Anya", "andrew", "Angel"]
- * }
- * }
- */
-public class Net_SearchUsers_Response extends Net_Response {
-
- private List logins = new ArrayList<>();
-
- public List getLogins() { return logins; }
- public void setLogins(List logins) { this.logins = logins; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.SolanaUserEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Base64;
-
-public class Net_AddUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
-
- /** TEST ONLY */
- private static final int TEST_BCH_LIMIT = 1_000_000;
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getBlockchainName() == null || req.getBlockchainName().isBlank()
- || req.getSolanaKey() == null || req.getSolanaKey().isBlank()
- || req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
- || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
- );
- }
-
- // blockchainName должен быть вида: -NNN
- if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BLOCKCHAIN_NAME",
- "blockchainName должен быть вида -NNN (пример: anya-001)"
- );
- }
-
- int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0)
- ? TEST_BCH_LIMIT
- : req.getBchLimit();
-
- try {
- // базовая валидация форматов ключей: Base64(32 bytes)
- byte[] solanaKey32 = Base64.getDecoder().decode(req.getSolanaKey());
- if (solanaKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_SOLANA_KEY",
- "solanaKey должен быть Base64(32 bytes)"
- );
- }
-
- byte[] blockchainKey32 = Base64.getDecoder().decode(req.getBlockchainKey());
- if (blockchainKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BLOCKCHAIN_KEY",
- "blockchainKey должен быть Base64(32 bytes)"
- );
- }
-
- byte[] deviceKey32 = Base64.getDecoder().decode(req.getDeviceKey());
- if (deviceKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_DEVICE_KEY",
- "deviceKey должен быть Base64(32 bytes)"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- SqliteDbController db = SqliteDbController.getInstance();
-
- try (Connection c = db.getConnection()) {
- c.setAutoCommit(false);
-
- // 1. Проверяем, что пользователя нет (case-insensitive)
- if (usersDAO.getByLogin(c, req.getLogin()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "USER_ALREADY_EXISTS",
- "Пользователь с таким login уже существует"
- );
- }
-
- // 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД)
- if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_ALREADY_EXISTS",
- "Пользователь с таким blockchainName уже существует"
- );
- }
-
- // 3. На всякий случай оставляем старую проверку blockchain_state,
- // потому что эта таблица нужна серверу (состояние цепочки/лимиты).
- if (stateDAO.getByBlockchainName(c, req.getBlockchainName()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_STATE_ALREADY_EXISTS",
- "blockchain_state уже существует"
- );
- }
-
- // 4. Создаём пользователя (все поля теперь лежат в solana_users)
- SolanaUserEntry user = new SolanaUserEntry();
- user.setLogin(req.getLogin());
- user.setBlockchainName(req.getBlockchainName());
- user.setSolanaKey(req.getSolanaKey());
- user.setBlockchainKey(req.getBlockchainKey());
- user.setDeviceKey(req.getDeviceKey());
-
- usersDAO.insert(c, user);
-
- // 5. Создаём INITIAL blockchain_state (для работы сервера)
- BlockchainStateEntry st = new BlockchainStateEntry();
- st.setBlockchainName(req.getBlockchainName());
- st.setLogin(req.getLogin());
- st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
- st.setLastBlockNumber(-1);
- st.setLastBlockHash(new byte[32]);
- st.setFileSizeBytes(0);
- st.setSizeLimit(limit);
- st.setUpdatedAtMs(System.currentTimeMillis());
-
- stateDAO.upsert(c, st);
-
- c.commit();
- }
-
- Net_AddUser_Response resp = new Net_AddUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
- req.getLogin(), req.getBlockchainName(), limit);
-
- return resp;
-
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_KEY_FORMAT",
- e.getMessage()
- );
- } catch (SQLException e) {
- log.error("❌ DB error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-public class Net_GetUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUser_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUser_Request req = (Net_GetUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- // тут логичнее BAD_REQUEST, но ты просил: "нет пользователя" тоже 200.
- // Поэтому BAD_REQUEST оставляем только на реально пустой login.
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
-
- try {
- SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
-
- Net_GetUser_Response resp = new Net_GetUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (u == null) {
- resp.setExists(false);
- log.info("ℹ️ GetUser: not found for login={}", req.getLogin());
- return resp;
- }
-
- // ВАЖНО:
- // - Поиск по login был case-insensitive,
- // - а тут возвращаем login/blockchainName как в БД (с исходным регистром).
- resp.setExists(true);
- resp.setLogin(u.getLogin());
- resp.setBlockchainName(u.getBlockchainName());
- resp.setSolanaKey(u.getSolanaKey());
- resp.setBlockchainKey(u.getBlockchainKey());
- resp.setDeviceKey(u.getDeviceKey());
-
- log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName());
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error GetUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error GetUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class Net_SearchUsers_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SearchUsers_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_SearchUsers_Request req = (Net_SearchUsers_Request) baseRequest;
-
- if (req.getPrefix() == null || req.getPrefix().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: prefix"
- );
- }
-
- String prefix = req.getPrefix().trim();
-
- try {
- SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
- List users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5
-
- List logins = new ArrayList<>();
- for (SolanaUserEntry u : users) {
- if (u != null && u.getLogin() != null) {
- logins.add(u.getLogin()); // регистр как в БД
- }
- }
-
- Net_SearchUsers_Response resp = new Net_SearchUsers_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setLogins(logins);
-
- log.info("✅ SearchUsers ok: prefix='{}' -> {}", prefix, logins.size());
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error SearchUsers", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error SearchUsers", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUserParam — получить один параметр пользователя.
- *
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) этот запрос не ограничивает просмотр параметров, т.к. проект в тестовом режиме.
- * Позже, вероятно, потребуется ограничить: кто и какие параметры может читать (сессия/права).
- * Но для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUserParam.
- *
- * Если найден:
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * }
- * }
- *
- * Если не найден:
- * status=404, payload пустой.
- */
-public class Net_GetUserParam_Response extends Net_Response {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListUserParams — получить все сохранённые параметры пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ ListUserParams — список всех параметров пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "params": [
- * {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * },
- * ...
- * ]
- * }
- * }
- */
-public class Net_ListUserParams_Response extends Net_Response {
-
- private String login;
- private List- params = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List
- getParams() { return params; }
- public void setParams(List
- params) { this.params = params; }
-
- public static class Item {
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-ed25519-public-key-32",
- * "signature": "base64-ed25519-signature-64"
- * }
- * }
- *
- * Подпись считается от UTF-8 строки:
- * USER_PARAMETER_PREFIX + login + param + time_ms + value
- */
-public class Net_UpsertUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
-
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на UpsertUserParam.
- *
- * Успех:
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_UpsertUserParam_Response extends Net_Response {
- // MVP: без payload. При желании позже можно добавить created/updated.
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-
-/**
- * GetUserParam — получить один параметр пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUserParam_Request req = (Net_GetUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param"
- );
- }
-
- String login = req.getLogin().trim();
- String param = req.getParam().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- UserParamEntry e = dao.getByLoginAndParam(c, login, param);
-
- if (e == null) {
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(404);
- return resp;
- }
-
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(e.getLogin());
- resp.setParam(e.getParam());
- resp.setTime_ms(e.getTimeMs());
- resp.setValue(e.getValue());
- resp.setDevice_key(e.getDeviceKey());
- resp.setSignature(e.getSignature());
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListUserParams — получить все параметры пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListUserParams_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_ListUserParams_Request req = (Net_ListUserParams_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- String login = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- List entries;
- try (Connection c = db.getConnection()) {
- entries = dao.getByLogin(c, login);
- }
-
- Net_ListUserParams_Response resp = new Net_ListUserParams_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(login);
-
- List items = new ArrayList<>();
- for (UserParamEntry e : entries) {
- Net_ListUserParams_Response.Item it = new Net_ListUserParams_Response.Item();
- it.setLogin(e.getLogin());
- it.setParam(e.getParam());
- it.setTime_ms(e.getTimeMs());
- it.setValue(e.getValue());
- it.setDevice_key(e.getDeviceKey());
- it.setSignature(e.getSignature());
- items.add(it);
- }
- resp.setParams(items);
-
- return resp;
-
- } catch (Exception e) {
- log.error("❌ Internal error ListUserParams", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.SolanaUserEntry;
-import shine.db.entities.UserParamEntry;
-import utils.config.ShineSignatureConstants;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Base64;
-
-/**
- * Net_UpsertUserParam_Handler
- *
- * Делает (MVP, без "сессий"):
- * 1) Проверка входных полей.
- * 2) Проверка подписи Ed25519 по device_key.
- * 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
- * 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
- *
- * ВАЖНО:
- * - НИКАКИХ ручных транзакций / BEGIN здесь нет.
- * - autoCommit=true, каждый statement завершённый сам по себе.
- * - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
- * наш финальный UPSERT просто вернёт 0 обновлённых строк.
- */
-public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()
- || req.getTime_ms() == null || req.getTime_ms() <= 0
- || req.getValue() == null
- || req.getDevice_key() == null || req.getDevice_key().isBlank()
- || req.getSignature() == null || req.getSignature().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param/time_ms/value/device_key/signature"
- );
- }
-
- final String login = req.getLogin().trim();
- final String param = req.getParam().trim();
- final long timeMs = req.getTime_ms();
- final String value = req.getValue();
- final String deviceKeyB64 = req.getDevice_key().trim();
- final String signatureB64 = req.getSignature().trim();
-
- try {
- // ---------------- Base64 decode ----------------
- byte[] pubKey32;
- byte[] sig64;
- try {
- pubKey32 = Base64.getDecoder().decode(deviceKeyB64);
- sig64 = Base64.getDecoder().decode(signatureB64);
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "device_key/signature должны быть Base64"
- );
- }
-
- if (pubKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_DEVICE_KEY",
- "device_key должен быть Base64(32 bytes)"
- );
- }
- if (sig64.length != 64) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_SIGNATURE",
- "signature должна быть Base64(64 bytes)"
- );
- }
-
- // ---------------- Signature verify ----------------
- String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
- + login
- + param
- + timeMs
- + value;
-
- byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
-
- boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "SIGNATURE_INVALID",
- "Подпись не прошла проверку"
- );
- }
-
- // ---------------- DB checks + upsert ----------------
- SqliteDbController db = SqliteDbController.getInstance();
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- // 1) user exists
- SolanaUserEntry user = usersDAO.getByLogin(c, login);
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- // 2) device key must match the user's stored deviceKey
- String userDeviceKey = user.getDeviceKey();
- if (userDeviceKey == null || userDeviceKey.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "USER_DEVICE_KEY_EMPTY",
- "У пользователя не задан deviceKey в БД"
- );
- }
-
- if (!userDeviceKey.trim().equals(deviceKeyB64)) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "DEVICE_KEY_MISMATCH",
- "device_key не соответствует пользователю"
- );
- }
-
- // 3) atomic upsert-if-newer
- UserParamEntry e = new UserParamEntry(
- login,
- param,
- timeMs,
- value,
- deviceKeyB64,
- signatureB64
- );
-
- int changed = paramsDAO.upsertIfNewer(c, e);
-
- Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (changed == 1) {
- log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
- } else {
- // 0 строк — значит в БД уже есть time_ms >= incoming
- log.info("ℹ️ UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
- }
-
- return resp;
- }
-
- } catch (SQLException e) {
- log.error("❌ DB error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-
-import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
-
-// --- NEW v2 session login ---
-import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionChallenge_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionLogin_Handler;
-
-// --- auth entities ---
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
-
-// --- NEW v2 entities ---
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
-
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
-
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
-
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
-
-// --- NEW: SearchUsers ---
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_SearchUsers_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Request;
-
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_GetUserParam_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
-
-// !!! подставь реальные пакеты/имена, как у тебя в проекте:
-//import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler;
-import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
-
-import java.util.Map;
-
-/**
- * JsonHandlerRegistry — единое место, где руками регистрируются
- * JSON-операции: op → handler и op → requestClass.
- */
-public final class JsonHandlerRegistry {
-
- // Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...)
- private static final Map HANDLERS = Map.ofEntries(
- Map.entry("AddUser", new Net_AddUser_Handler()),
- Map.entry("GetUser", new Net_GetUser_Handler()),
- Map.entry("SearchUsers", new Net_SearchUsers_Handler()),
-
- // --- auth ---
- Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
- Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()),
- Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
- Map.entry("ListSessions", new Net_ListSessions_Handler()),
-
- // --- login to existing session in 2 steps ---
- Map.entry("SessionChallenge", new Net_SessionChallenge_Handler()),
- Map.entry("SessionLogin", new Net_SessionLogin_Handler()),
-
- // --- blockchain ---
- Map.entry("AddBlock", new Net_AddBlock_Handler()),
-
- // --- userParams ---
- Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()),
- Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
- Map.entry("ListUserParams", new Net_ListUserParams_Handler())
-
- // --- subscriptions ---
-// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
- );
-
- private static final Map> REQUEST_TYPES = Map.ofEntries(
- Map.entry("AddUser", Net_AddUser_Request.class),
- Map.entry("GetUser", Net_GetUser_Request.class),
- Map.entry("SearchUsers", Net_SearchUsers_Request.class),
-
- // --- auth ---
- Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),
- Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class),
- Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
- Map.entry("ListSessions", Net_ListSessions_Request.class),
-
- // --- NEW v2 ---
- Map.entry("SessionChallenge", Net_SessionChallenge_Request.class),
- Map.entry("SessionLogin", Net_SessionLogin_Request.class),
-
- // --- blockchain ---
- Map.entry("AddBlock", Net_AddBlock_Request.class),
-
- // --- userParams ---
- Map.entry("UpsertUserParam", Net_UpsertUserParam_Request.class),
- Map.entry("GetUserParam", Net_GetUserParam_Request.class),
- Map.entry("ListUserParams", Net_ListUserParams_Request.class),
-
- // --- subscriptions ---
- Map.entry("ListSubscribedChannels", Net_GetSubscribedChannels_Request.class)
- );
-
- private JsonHandlerRegistry() { }
-
- public static Map getHandlers() {
- return HANDLERS;
- }
-
- public static Map> getRequestTypes() {
- return REQUEST_TYPES;
- }
-}
-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.Net_Exception_Response;
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-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-сообщений.
- *
- * 1) Парсит общий пакет (op, requestId, payload).
- * 2) По op выбирает класс запроса и хэндлер.
- * 3) Собирает "плоский" объект: op + requestId + поля из payload.
- * 4) Маппит его в NetRequest через ObjectMapper.
- * 5) Вызывает хэндлер, получает NetResponse.
- * 6) Собирает 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()
- .setSerializationInclusion(JsonInclude.Include.NON_NULL);
-
- private static final Map JSON_HANDLERS =
- JsonHandlerRegistry.getHandlers();
-
- private static final Map> JSON_REQUEST_TYPES =
- JsonHandlerRegistry.getRequestTypes();
-
- private JsonInboundProcessor() {
- // utility
- }
-
- public static String processJson(String json, ConnectionContext ctx) {
- String op = null;
- String requestId = null;
-
- // Для лога полезно знать, кто прислал (хотя бы login/sessionId, если есть)
- String ctxLogin = safe(ctx != null ? ctx.getLogin() : null);
- String ctxSessionId = safe(ctx != null ? ctx.getSessionId() : null);
-
- try {
- if (json == null || json.isBlank()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- null,
- null,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_JSON",
- "Пустое JSON-сообщение"
- );
-
- String out = writeResponse(err);
-
- // DEBUG: что пришло / что ушло
- if (log.isDebugEnabled()) {
- log.debug("JSON IN (login={}, sessionId={}): ", ctxLogin, ctxSessionId);
- log.debug("JSON OUT (login={}, sessionId={}): {}", ctxLogin, ctxSessionId, shorten(out, 1200));
- }
- return out;
- }
-
- // DEBUG: сырой вход (обрезаем, чтобы не убить лог)
- if (log.isDebugEnabled()) {
- log.debug("JSON IN (login={}, sessionId={}): {}", ctxLogin, ctxSessionId, shorten(json, 1200));
- }
-
- // 1) Парсим общий пакет
- JsonNode root = JSON_MAPPER.readTree(json);
-
- // 2) op и requestId из корня
- op = getTextOrNull(root, "op");
- requestId = getTextOrNull(root, "requestId");
-
- if (op == null || op.isEmpty()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- null,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "NO_OP",
- "Поле 'op' отсутствует или пустое"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- JsonMessageHandler handler = JSON_HANDLERS.get(op);
- Class extends Net_Request> reqClass = JSON_REQUEST_TYPES.get(op);
-
- if (handler == null || reqClass == null) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "UNKNOWN_OP",
- "Неизвестная операция: " + op
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // 3) Берём payload
- JsonNode payloadNode = root.get("payload");
- if (payloadNode == null || payloadNode.isNull()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "NO_PAYLOAD",
- "Поле 'payload' отсутствует"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
- if (!payloadNode.isObject()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "BAD_PAYLOAD",
- "Поле 'payload' должно быть объектом"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // 3.1 Собираем "плоский" объект для маппинга в NetRequest:
- // op + requestId + поля из payload
- ObjectNode merged = JSON_MAPPER.createObjectNode();
-
- // Добавляем op и requestId, чтобы они попали в NetRequest
- merged.put("op", op);
- if (requestId != null) merged.put("requestId", requestId);
-
- // Добавляем все поля из payload внутрь
- merged.setAll((ObjectNode) payloadNode);
-
- // 4) Маппим в конкретный класс NetRequest
- Net_Request request;
- try {
- request = JSON_MAPPER.treeToValue(merged, reqClass);
- } catch (Exception mapErr) {
- // Важно: вот это часто “теряется”, если не логировать отдельно
- log.error("❌ JSON map error (op={}, requestId={}, login={}, sessionId={}): merged={}",
- op, safe(requestId), ctxLogin, ctxSessionId, shorten(merged.toString(), 1200), mapErr);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "BAD_REQUEST_FORMAT",
- "Некорректный формат запроса: не удалось распарсить поля payload"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // DEBUG: нормализованный запрос (уже распарсен)
- if (log.isDebugEnabled()) {
- log.debug("REQ OBJ (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(safeToString(request), 1200));
- }
-
- // 5) Вызываем хэндлер
- Net_Response response;
- try {
- response = handler.handle(request, ctx);
- } catch (Exception handlerError) {
- // ✅ Вот тут как раз и должны “появляться ошибки в логере”
- log.error("💥 Handler error (op={}, requestId={}, login={}, sessionId={})",
- op, safe(requestId), ctxLogin, ctxSessionId, handlerError);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_HANDLER_ERROR",
- "Неожиданная ошибка при обработке операции: " + op
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // На всякий случай: если хэндлер не выставил op/requestId
- if (response.getOp() == null) response.setOp(op);
- if (response.getRequestId() == null) response.setRequestId(requestId);
-
- // 6) Универсальная сборка ответа
- String out = writeResponse(response);
-
- // DEBUG: ответ ушёл
- if (log.isDebugEnabled()) {
- log.debug("RESP OBJ (login={}, sessionId={}, op={}, requestId={}, status={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), response.getStatus(), shorten(safeToString(response), 1200));
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}, status={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), response.getStatus(), shorten(out, 1200));
- }
-
- return out;
-
- } catch (Exception e) {
- // ✅ Любая неожиданная ошибка парсинга/обработки — в лог
- log.error("❌ JSON processing error (op={}, requestId={}, login={}, sessionId={})",
- safe(op), safe(requestId), safe(ctxLogin), safe(ctxSessionId), e);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op != null ? op : "Unknown",
- requestId,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
-
- String out = writeResponse(err);
-
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
-
- return out;
- }
- }
-
- // --- helpers ---
-
- private static String getTextOrNull(JsonNode node, String field) {
- if (node == null || !node.has(field) || node.get(field).isNull()) return null;
- return node.get(field).asText();
- }
-
- /**
- * Унифицированная сериализация любого NetResponse в формат:
- * {
- * "op": ...,
- * "requestId": ...,
- * "status": ...,
- * "payload": { ... }
- * }
- */
- private static String writeResponse(Net_Response response) {
- try {
- // Конвертируем полный объект ответа в 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);
-
- // payload — это всё, что осталось от full (может быть пустым объектом {})
- root.set("payload", full);
-
- return JSON_MAPPER.writeValueAsString(root);
-
- } catch (Exception e) {
- // Совсем аварийный случай — сериализация ответа сломалась.
- log.error("❌ Response serialization error (op={}, requestId={})",
- safe(response != null ? response.getOp() : null),
- safe(response != null ? response.getRequestId() : null),
- e);
-
- return "{\"op\":\"" + safe(response != null ? response.getOp() : null) +
- "\",\"requestId\":\"" + safe(response != null ? response.getRequestId() : null) +
- "\",\"status\":" + (response != null ? response.getStatus() : 500) +
- ",\"payload\":{\"code\":\"SERIALIZATION_ERROR\",\"message\":\"Ошибка сериализации ответа\"}}";
- }
- }
-
- private static String safe(String s) {
- return s != null ? s : "";
- }
-
- private static String shorten(String s, int max) {
- if (s == null) return "";
- if (s.length() <= max) return s;
- return s.substring(0, Math.max(0, max)) + "...(+" + (s.length() - max) + " chars)";
- }
-
- private static String safeToString(Object o) {
- if (o == null) return "null";
- try {
- // Чтобы не плодить огромные логи и не утыкаться в циклические ссылки —
- // логируем как JSON, если возможно.
- return JSON_MAPPER.writeValueAsString(o);
- } catch (Exception ignore) {
- return String.valueOf(o);
- }
- }
-}
-package server.logic.ws_protocol.JSON.utils;
-
-import shine.db.entities.SolanaUserEntry;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-
-public final class AuthSignatures {
-
- private AuthSignatures() {}
-
- /** preimage для CreateAuthSession(v2): "AUTH_CREATE_SESSION:login:timeMs:authNonce" */
- public static byte[] preimageCreateAuthSession(String login, long timeMs, String authNonce) {
- String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
- return preimageStr.getBytes(StandardCharsets.UTF_8);
- }
-
- /** Декод base64 / base64url (если надо — подстрой под твой decodeBase64Any) */
- public static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
- if (s == null) throw new IllegalArgumentException("base64 is null");
- String x = s.trim();
- if (x.isEmpty()) throw new IllegalArgumentException("base64 is empty");
-
- try {
- return Base64.getDecoder().decode(x);
- } catch (IllegalArgumentException e1) {
- // пробуем base64url без паддинга
- return Base64.getUrlDecoder().decode(x);
- }
- }
-
- /**
- * Проверка подписи CreateAuthSession(v2) по deviceKey пользователя.
- * Подпись проверяется над preimageCreateAuthSession(...).
- */
- public static boolean verifyCreateAuthSessionSignature(
- SolanaUserEntry user,
- String login,
- String authNonce,
- long timeMs,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // user.getDeviceKey() — base64 публичного ключа (32 байта)
- byte[] publicKey32 = decodeBase64Any(user.getDeviceKey());
- byte[] signature64 = decodeBase64Any(signatureB64);
-
- byte[] preimage = preimageCreateAuthSession(login, timeMs, authNonce);
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-}
-package server.logic.ws_protocol.JSON.utils;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Фабрика ошибок для JSON-протокола.
- * Создаёт единообразные NetExceptionResponse.
- */
-public final class NetExceptionResponseFactory {
-
- private NetExceptionResponseFactory() {
- // запрет на создание объектов
- }
-
- public static Net_Exception_Response error(Net_Request req,
- int status,
- String code,
- String message) {
-
- Net_Exception_Response resp = new Net_Exception_Response();
-
- // ✅ НЕ падаем, даже если req == null
- if (req != null) {
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- } else {
- resp.setOp(null);
- resp.setRequestId(null);
- }
-
- resp.setStatus(status);
- resp.setCode(code);
- resp.setMessage(message);
- return resp;
- }
-
- /**
- * Вариант для случаев, когда NetRequest ещё не распарсен,
- * но мы уже знаем op и requestId (или они null).
- */
- public static Net_Exception_Response error(String op,
- String requestId,
- int status,
- String code,
- String message) {
-
- Net_Exception_Response resp = new Net_Exception_Response();
- resp.setOp(op);
- resp.setRequestId(requestId);
- resp.setStatus(status);
- resp.setCode(code);
- resp.setMessage(message);
- return resp;
- }
-}
-package server.logic.ws_protocol;
-
-/**
- * WireCodes — константы бинарного протокола поверх WebSocket.
- *.
- * Формат входящего сообщения:
- * [4] int opCode (big-endian)
- * [*] payload
- *.
- * Ответ сервера:
- * ровно [4] int statusCode (big-endian)
- */
-public final class WireCodes {
- private WireCodes() {}
-
- public static final class Op {
- public static final int PING = 0;
- public static final int ADD_BLOCK = 1;
- public static final int GET_BLOCKCHAIN = 2;
- public static final int SEARCH_USERS = 30;
- public static final int GET_LAST_BLOCK_INFO = 31;
- private Op() {}
- }
-
- public static final class Status {
- public static final int PONG = 100; // ответ на PING
-// public static final int OK = 200; // успех
-
- public static final int ALREADY_EXISTS = 409; // пришёл блок < N+1
- public static final int NON_SEQUENTIAL = 412; // пришёл блок > N+1
- public static final int NOT_FOUND = 422; // Нет такого полбзователя - типо добавляем блок к которому нет пользователя - хотя на деле такой статус наверное никогда не вернётся, тк это раньше проверяется
-
-
- private Status() {}
-
-
-
-
- // ============================================================
- // 🟢 УСПЕШНЫЕ ОПЕРАЦИИ
- // ============================================================
-
- /** ✅ Блок успешно добавлен в цепочку. */
- public static final int OK = 200;
-
- /** 🌱 Создана новая цепочка (первый блок-заголовок принят). */
- public static final int CHAIN_CREATED = 201;
-
- /**
- * 🔁 Такой блок уже существует.
- * Клиент может считать это успешным ответом:
- * - сервер возвращает 8 байт: [4] код (202) + [4] номер последнего блока (int)
- * - клиент обновляет свой lastBlockNumber и не пересылает этот блок снова. */
- public static final int BLOCK_ALREADY_EXISTS = 202; // плюс к кодуследом возвращается номер последнего блока на сервере
-
-
- // ============================================================
- // 🟡 ЛОГИЧЕСКИЕ / ПРОТОКОЛЬНЫЕ ОШИБКИ
- // ============================================================
-
- /** ⚠️ Нарушена последовательность — пришёл блок с номером > ожидаемого.
- * Сервер вернёт 8 байт: [4] код (409) + [4] последний номер блока.
- * Клиент должен дослать недостающие блоки. */
- public static final int OUT_OF_SEQUENCE = 409; // плюс к кодуследом возвращается номер последнего блока на сервере
-
- /** ❌ Некорректные или неполные данные в запросе. */
- public static final int BAD_REQUEST = 400;
-
- /** 🚫 Цепочка с указанным blockchainId не найдена. */
- public static final int CHAIN_NOT_FOUND = 404;
-
- /** 🧩 Несовпадение blockchainId между заголовком блока и телом. */
- public static final int INVALID_BLOCKCHAIN_ID = 421;
-
- /** ❌ Ошибка верификации блока — хэш или подпись не совпали.
- * 🔐 Ошибка хэша: SHA-256(preimage) не совпал с переданным hash32.
- * 🔏 Ошибка подписи Ed25519 — блок не прошёл криптографическую проверку. */
- public static final int UNVERIFIED = 422;
-
-
- /** 🙅 Некорректный логин (пустой, неверный формат, недопустимые символы). По сути вообще не может быть, тк логин проверяют при создании в другом блокчейне*/
- public static final int BAD_LOGIN = 462;
-
-
- // ============================================================
- // 🔴 СИСТЕМНЫЕ ОШИБКИ / ОГРАНИЧЕНИЯ
- // ============================================================
-
- // ============================================================
- // 🔴 СИСТЕМНЫЕ ОШИБКИ / ОГРАНИЧЕНИЯ
- // ============================================================
-
- /** 💾 Достигнут лимит размера блокчейна. */
- public static final int BLOCKCHAIN_FULL = 507;
-
- /** 🧱 Ошибка при сохранении или обновлении данных на сервере (файлы, JSON и т.п.). */
- public static final int SERVER_DATA_ERROR = 501;
-
- /** 💥 Общая внутренняя ошибка сервера (необработанное исключение). */
- public static final int INTERNAL_ERROR = 500;
- }
-
-}
-
-package server.ws;
-
-import org.eclipse.jetty.websocket.api.Session;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-import server.logic.ws_protocol.JSON.ConnectionContext;
-import shine.db.entities.SolanaUserEntry;
-
-import java.net.SocketAddress;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * Утилита для работы с WebSocket-подключениями.
- *
- * Цель этой версии:
- * - всегда логировать "кто закрыл" / "что закрывали" / "в каком состоянии был WS";
- * - логировать исключения так, чтобы было видно первопричину;
- * - не терять контекст из-за ctx.reset() (сначала снимаем "снимок" полей).
- */
-public final class WsConnectionUtils {
-
- private static final Logger log = LoggerFactory.getLogger(WsConnectionUtils.class);
-
- /** Счётчик событий закрытия (удобно коррелировать логи). */
- private static final AtomicLong CLOSE_SEQ = new AtomicLong(0);
-
- private WsConnectionUtils() {
- // utility
- }
-
- public static void closeConnection(ConnectionContext ctx, int statusCode, String reason) {
- closeConnection(ctx, statusCode, reason, null, "UNKNOWN");
- }
-
- /**
- * Расширенное закрытие с указанием инициатора и причины (Throwable).
- *
- * @param ctx контекст
- * @param statusCode код закрытия
- * @param reason причина (пойдёт в close frame + логи)
- * @param cause исключение/первопричина (если закрываем из catch)
- * @param initiator строка "кто инициировал" (handler/op/requestId/etc.)
- */
- public static void closeConnection(ConnectionContext ctx,
- int statusCode,
- String reason,
- Throwable cause,
- String initiator) {
- if (ctx == null) return;
-
- final long closeId = CLOSE_SEQ.incrementAndGet();
-
- // --- СНИМОК КОНТЕКСТА ДО reset() ---
- final Session ws = ctx.getWsSession();
-
- final String sessionId = safeString(ctx.getSessionId());
- final int authStatus = safeAuthStatus(ctx);
-
- final SolanaUserEntry user = ctx.getSolanaUser();
- final String login = (user != null ? safeString(user.getLogin()) : "");
-
- final String activeSessionId =
- (ctx.getActiveSession() != null ? safeString(ctx.getActiveSession().getSessionId()) : "");
-
- final boolean wsPresent = (ws != null);
- final boolean wsOpen = (ws != null && safeIsOpen(ws));
- final String wsInfo = formatWsInfo(ws);
-
- final String threadName = Thread.currentThread().getName();
- final int ctxId = System.identityHashCode(ctx);
-
- // Логируем "начало закрытия" всегда, чтобы видеть даже случаи "ws уже закрыт"
- if (cause != null) {
- log.warn("WS_CLOSE#{} BEGIN initiator={} thread={} ctxId={} login={} sessionId={} activeSessionId={} authStatus={} statusCode={} reason={} wsPresent={} wsOpen={} wsInfo={}",
- closeId, initiator, threadName, ctxId, login, sessionId, activeSessionId, authStatus, statusCode, reason, wsPresent, wsOpen, wsInfo, cause);
- } else {
- log.info("WS_CLOSE#{} BEGIN initiator={} thread={} ctxId={} login={} sessionId={} activeSessionId={} authStatus={} statusCode={} reason={} wsPresent={} wsOpen={} wsInfo={}",
- closeId, initiator, threadName, ctxId, login, sessionId, activeSessionId, authStatus, statusCode, reason, wsPresent, wsOpen, wsInfo);
- }
-
- // --- ШАГ 1: убрать из реестра (чтобы новые сообщения не шли в мёртвый контекст) ---
- try {
- ActiveConnectionsRegistry.getInstance().remove(ctx);
- log.debug("WS_CLOSE#{} registry.remove OK ctxId={} sessionId={} login={}", closeId, ctxId, sessionId, login);
- } catch (Exception e) {
- log.warn("WS_CLOSE#{} registry.remove FAIL ctxId={} sessionId={} login={}", closeId, ctxId, sessionId, login, e);
- }
-
- // --- ШАГ 2: закрыть WS (если открыт) ---
- if (ws != null) {
- if (safeIsOpen(ws)) {
- try {
- ws.close(statusCode, safeString(reason));
- log.info("WS_CLOSE#{} ws.close OK ctxId={} sessionId={} login={} statusCode={} reason={}",
- closeId, ctxId, sessionId, login, statusCode, reason);
- } catch (Exception e) {
- log.warn("WS_CLOSE#{} ws.close FAIL ctxId={} sessionId={} login={} statusCode={} reason={} wsInfo={}",
- closeId, ctxId, sessionId, login, statusCode, reason, wsInfo, e);
- }
- } else {
- log.info("WS_CLOSE#{} ws already closed ctxId={} sessionId={} login={} wsInfo={}",
- closeId, ctxId, sessionId, login, wsInfo);
- }
- }
-
- // --- ШАГ 3: очистить контекст (в конце, чтобы не потерять поля в логах выше) ---
- try {
- ctx.reset();
- log.debug("WS_CLOSE#{} ctx.reset OK ctxId={} (was sessionId={}, login={})", closeId, ctxId, sessionId, login);
- } catch (Exception e) {
- log.warn("WS_CLOSE#{} ctx.reset FAIL ctxId={} (was sessionId={}, login={})", closeId, ctxId, sessionId, login, e);
- }
-
- log.info("WS_CLOSE#{} END initiator={} ctxId={} sessionId={} login={}", closeId, initiator, ctxId, sessionId, login);
- }
-
- private static String safeString(String s) {
- return (s == null ? "" : s);
- }
-
- private static int safeAuthStatus(ConnectionContext ctx) {
- try {
- return ctx.getAuthenticationStatus();
- } catch (Exception e) {
- return -999;
- }
- }
-
- private static boolean safeIsOpen(Session ws) {
- try {
- return ws.isOpen();
- } catch (Exception e) {
- return false;
- }
- }
-
- private static String formatWsInfo(Session ws) {
- if (ws == null) return "null";
-
- String remote = "";
- String local = "";
- try {
- SocketAddress ra = ws.getRemoteAddress();
- remote = (ra != null ? ra.toString() : "");
- } catch (Exception ignored) { }
-
- try {
- SocketAddress la = ws.getLocalAddress();
- local = (la != null ? la.toString() : "");
- } catch (Exception ignored) { }
-
- return "remote=" + remote + ", local=" + local;
- }
-}
diff --git a/SHiNE-server/shine-server-net-protocol/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/all_files.txt b/SHiNE-server/shine-server-net-protocol/src/main/java/server/all_files.txt
deleted file mode 100644
index cce7330..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/all_files.txt
+++ /dev/null
@@ -1,4742 +0,0 @@
-// file: server/logic/ws_protocol/B64.java
-package server.logic.ws_protocol;
-
-import java.util.Base64;
-
-/**
- * Единая утилита Base64 для всего WS-протокола.
- *
- * Правило: используем ТОЛЬКО стандартный Base64 (RFC 4648):
- * - алфавит: A-Z a-z 0-9 + /
- * - padding: "=" (Java encoder добавляет по умолчанию)
- *
- * Никаких Base64url ("-" "_") и никаких "без padding" в протоколе.
- */
-public final class B64 {
-
- private B64() {}
-
- /** Кодирует байты в стандартный Base64 (с padding). */
- public static String enc(byte[] bytes) {
- if (bytes == null) throw new IllegalArgumentException("bytes == null");
- return Base64.getEncoder().encodeToString(bytes);
- }
-
- /** Декодирует стандартный Base64 в байты. */
- public static byte[] dec(String b64) {
- if (b64 == null) throw new IllegalArgumentException("base64 == null");
- String s = b64.trim();
- if (s.isEmpty()) throw new IllegalArgumentException("base64 == empty");
- // Строго стандартный декодер (не url-safe)
- return Base64.getDecoder().decode(s);
- }
-
- /** Декодирует и проверяет, что длина результата ровно expectedLen. */
- public static byte[] decLen(String b64, int expectedLen, String fieldName) {
- byte[] out = dec(b64);
- if (out.length != expectedLen) {
- throw new IllegalArgumentException(fieldName + " must decode to " + expectedLen + " bytes, got " + out.length);
- }
- return out;
- }
-
- public static byte[] dec32(String b64, String fieldName) {
- return decLen(b64, 32, fieldName);
- }
-
- public static byte[] dec64(String b64, String fieldName) {
- return decLen(b64, 64, fieldName);
- }
-}
-package server.logic.ws_protocol;
-
-import java.util.Base64;
-
-/**
- * Единая утилита Base64 для всего WS-протокола.
- *
- * ВАЖНО:
- * - Используем ТОЛЬКО стандартный Base64 (RFC 4648) алфавит: '+' и '/'.
- * - Без padding '=' (чтобы строки были короче и стабильнее для JSON).
- * - Декодер при этом спокойно принимает и с '=' и без '='.
- */
-public final class Base64Ws {
-
- private static final Base64.Encoder ENC = Base64.getEncoder().withoutPadding();
- private static final Base64.Decoder DEC = Base64.getDecoder();
-
- private Base64Ws() {}
-
- public static String encode(byte[] bytes) {
- if (bytes == null) throw new IllegalArgumentException("bytes == null");
- return ENC.encodeToString(bytes);
- }
-
- public static byte[] decode(String b64) throws IllegalArgumentException {
- if (b64 == null) throw new IllegalArgumentException("base64 is null");
- String s = b64.trim();
- if (s.isEmpty()) throw new IllegalArgumentException("base64 is empty");
- return DEC.decode(s);
- }
-
- public static byte[] decodeLen(String b64, int expectedLen, String fieldName) throws IllegalArgumentException {
- byte[] v = decode(b64);
- if (v.length != expectedLen) {
- String f = (fieldName == null || fieldName.isBlank()) ? "value" : fieldName;
- throw new IllegalArgumentException(f + " must be " + expectedLen + " bytes, got " + v.length);
- }
- return v;
- }
-}
-package server.logic.ws_protocol.JSON;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-/**
- * Реестр активных подключений (только авторизованные).
- */
-public final class ActiveConnectionsRegistry {
-
- private static final Logger log = LoggerFactory.getLogger(ActiveConnectionsRegistry.class);
-
- private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry();
-
- public static ActiveConnectionsRegistry getInstance() {
- return INSTANCE;
- }
-
- private ActiveConnectionsRegistry() {
- // singleton
- }
-
- // sessionId (String) -> ConnectionContext
- private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>();
-
- // login (String) -> множество ConnectionContext для этого пользователя
- private final ConcurrentHashMap> byLogin = new ConcurrentHashMap<>();
-
- /**
- * Зарегистрировать авторизованное подключение.
- * Ожидается, что в ctx уже выставлены login и sessionId.
- */
- public void register(ConnectionContext ctx) {
- if (ctx == null) return;
-
- String sessionId = ctx.getSessionId();
- String login = ctx.getLogin();
-
- if (sessionId == null || sessionId.isBlank() || login == null || login.isBlank()) {
- log.debug("register skipped: bad ctx fields (login='{}', sessionId='{}')", login, sessionId);
- return;
- }
-
- // ✅ Если кто-то перерегистрировал тот же sessionId — вычищаем старый ctx из byLogin
- ConnectionContext prev = bySessionId.put(sessionId, ctx);
- if (prev != null && prev != ctx) {
- String prevLogin = prev.getLogin();
- if (prevLogin != null && !prevLogin.isBlank()) {
- Set prevSet = byLogin.get(prevLogin);
- if (prevSet != null) {
- prevSet.remove(prev);
- if (prevSet.isEmpty()) {
- byLogin.remove(prevLogin);
- }
- }
- }
- log.warn("sessionId reused: replaced previous ctx (sessionId={}, prevLogin={}, newLogin={})",
- sessionId, prevLogin, login);
- }
-
- byLogin
- .computeIfAbsent(login, id -> new CopyOnWriteArraySet<>())
- .add(ctx);
-
- log.debug("registered ctx (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Удалить подключение по контексту (например, при onClose).
- */
- public void remove(ConnectionContext ctx) {
- if (ctx == null) return;
-
- String sessionId = ctx.getSessionId();
- String login = ctx.getLogin();
-
- if (sessionId != null && !sessionId.isBlank()) {
- ConnectionContext removed = bySessionId.remove(sessionId);
-
- // Если в мапе лежал другой ctx под тем же sessionId — не трогаем его byLogin
- if (removed != null && removed != ctx) {
- log.debug("remove(ctx): sessionId mapped to another ctx, skip byLogin cleanup (sessionId={})", sessionId);
- return;
- }
- }
-
- if (login != null && !login.isBlank()) {
- Set set = byLogin.get(login);
- if (set != null) {
- set.remove(ctx);
- if (set.isEmpty()) {
- byLogin.remove(login);
- }
- }
- }
-
- log.debug("removed ctx (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Удалить подключение по sessionId.
- */
- public void removeBySessionId(String sessionId) {
- if (sessionId == null || sessionId.isBlank()) return;
-
- ConnectionContext ctx = bySessionId.remove(sessionId);
- if (ctx == null) return;
-
- String login = ctx.getLogin();
- if (login != null && !login.isBlank()) {
- Set set = byLogin.get(login);
- if (set != null) {
- set.remove(ctx);
- if (set.isEmpty()) {
- byLogin.remove(login);
- }
- }
- }
-
- log.debug("removed by sessionId (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Получить контекст по sessionId.
- */
- public ConnectionContext getBySessionId(String sessionId) {
- if (sessionId == null || sessionId.isBlank()) return null;
- return bySessionId.get(sessionId);
- }
-
- /**
- * Получить все активные подключения пользователя по login.
- */
- public Set getByLogin(String login) {
- if (login == null || login.isBlank()) return Set.of();
- Set set = byLogin.get(login);
- return (set == null) ? Set.of() : set; // CopyOnWriteArraySet можно отдавать как есть
- }
-}
-package server.logic.ws_protocol.JSON;
-
-import org.eclipse.jetty.websocket.api.Session;
-import shine.db.entities.SolanaUserEntry;
-import shine.db.entities.ActiveSessionEntry;
-
-/**
- * ConnectionContext — контекст состояния одного WebSocket-соединения.
- * Живёт ровно столько же, сколько живёт подключение.
- *
- * Важно (v2):
- * - Авторизация всегда 2 шага:
- * A) Создание новой сессии через deviceKey:
- * AuthChallenge(login) -> ctx.authNonce
- * CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
- *
- * B) Вход в существующую сессию через sessionKey:
- * SessionChallenge(sessionId) -> ctx.sessionLoginNonce + ctx.sessionLoginSessionId + expiresAt
- * SessionLogin(...) -> проверка подписи sessionKey по pubkey из БД -> ctx.AUTH_STATUS_USER
- */
-public class ConnectionContext {
-
- // Статусы аутентификации
- public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
- public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
- public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
-
- // Полный пользователь из БД (solana_users)
- private SolanaUserEntry solanaUserEntry;
-
- // Активная сессия из БД (active_sessions)
- private ActiveSessionEntry activeSessionEntry;
-
- /**
- * Идентификатор сессии — base64-строка от 32 байт.
- * Заполняется после успешного входа (AUTH_STATUS_USER).
- */
- private String sessionId;
-
- /**
- * Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
- * используется на шаге CreateAuthSession для проверки подписи deviceKey.
- */
- private String authNonce;
-
- /* ===================== SessionLogin challenge (v2) ===================== */
-
- /**
- * Одноразовый nonce, выданный на шаге SessionChallenge(sessionId),
- * используется на шаге SessionLogin для проверки подписи sessionKey.
- */
- private String sessionLoginNonce;
-
- /**
- * sessionId, для которого был выдан sessionLoginNonce.
- * Нужен, чтобы SessionLogin не мог "подставить" другой sessionId.
- */
- private String sessionLoginSessionId;
-
- /**
- * Время истечения sessionLoginNonce (мс с 1970-01-01).
- * Если текущее время > expiresAt, то nonce считается недействительным.
- */
- private long sessionLoginNonceExpiresAtMs;
-
- /* ====================================================================== */
-
- /**
- * Текущий статус аутентификации.
- * См. константы AUTH_STATUS_*
- */
- private int authenticationStatus = AUTH_STATUS_NONE;
-
- /**
- * WebSocket-сессия Jetty для данного подключения.
- * Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту.
- */
- private Session wsSession;
-
- // --- WebSocket Session ---
-
- public Session getWsSession() {
- return wsSession;
- }
-
- public void setWsSession(Session wsSession) {
- this.wsSession = wsSession;
- }
-
- // --- SolanaUser / ActiveSession ---
-
- public SolanaUserEntry getSolanaUser() {
- return solanaUserEntry;
- }
-
- public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
- this.solanaUserEntry = solanaUserEntry;
- }
-
- public ActiveSessionEntry getActiveSession() {
- return activeSessionEntry;
- }
-
- public void setActiveSession(ActiveSessionEntry activeSessionEntry) {
- this.activeSessionEntry = activeSessionEntry;
- }
-
- // --- Удобный геттер для логина ---
-
- public String getLogin() {
- return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
- }
-
- // --- sessionId ---
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- // --- authNonce ---
-
- public String getAuthNonce() {
- return authNonce;
- }
-
- public void setAuthNonce(String authNonce) {
- this.authNonce = authNonce;
- }
-
- // --- sessionLoginNonce (v2) ---
-
- public String getSessionLoginNonce() {
- return sessionLoginNonce;
- }
-
- public void setSessionLoginNonce(String sessionLoginNonce) {
- this.sessionLoginNonce = sessionLoginNonce;
- }
-
- public String getSessionLoginSessionId() {
- return sessionLoginSessionId;
- }
-
- public void setSessionLoginSessionId(String sessionLoginSessionId) {
- this.sessionLoginSessionId = sessionLoginSessionId;
- }
-
- public long getSessionLoginNonceExpiresAtMs() {
- return sessionLoginNonceExpiresAtMs;
- }
-
- public void setSessionLoginNonceExpiresAtMs(long sessionLoginNonceExpiresAtMs) {
- this.sessionLoginNonceExpiresAtMs = sessionLoginNonceExpiresAtMs;
- }
-
- // --- auth status ---
-
- public int getAuthenticationStatus() {
- return authenticationStatus;
- }
-
- public void setAuthenticationStatus(int authenticationStatus) {
- this.authenticationStatus = authenticationStatus;
- }
-
- public boolean isAuthenticatedUser() {
- return authenticationStatus == AUTH_STATUS_USER;
- }
-
- public boolean isAnonymous() {
- return authenticationStatus == AUTH_STATUS_NONE;
- }
-
- public void reset() {
- solanaUserEntry = null;
- activeSessionEntry = null;
-
- sessionId = null;
- authNonce = null;
-
- sessionLoginNonce = null;
- sessionLoginSessionId = null;
- sessionLoginNonceExpiresAtMs = 0;
-
- authenticationStatus = AUTH_STATUS_NONE;
- wsSession = null;
- }
-
- @Override
- public String toString() {
- return "ConnectionContext{" +
- "login='" + getLogin() + '\'' +
- ", sessionId=" + sessionId +
- ", authenticationStatus=" + authenticationStatus +
- '}';
- }
-}
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех событий (event).
- * Общие поля: op и payload.
- *.
- * Формат JSON (event):
- * {
- * "op": "...",
- * "payload": { ... }
- * }
- */
-public abstract class Net_Event {
-
- /** Имя операции / события (op). */
- private String op;
-
- /**
- * Произвольные данные.
- * В JSON это поле "payload".
- */
- private Object payload;
-
- // --- getters / setters ---
-
- public String getOp() {
- return op;
- }
-
- public void setOp(String op) {
- this.op = op;
- }
-
- public Object getPayload() {
- return payload;
- }
-
- public void setPayload(Object payload) {
- this.payload = payload;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Ответ с ошибкой (любой отказ).
- *.
- * В payload будет:
- * {
- * "code": "...",
- * "message": "..."
- * }
- */
-public class Net_Exception_Response extends Net_Response {
-
- 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;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех запросов (client → server).
- *.
- * Наследуется от NetEvent и добавляет requestId.
- *.
- * Формат JSON (request):
- * {
- * "op": "...",
- * "requestId": "...",
- * "payload": { ... }
- * }
- */
-public abstract class Net_Request extends Net_Event {
-
- /** Идентификатор запроса, чтобы связать запрос и ответ. */
- private String requestId;
-
- // --- getters / setters ---
-
- public String getRequestId() {
- return requestId;
- }
-
- public void setRequestId(String requestId) {
- this.requestId = requestId;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех ответов (server → client).
- *.
- * Наследуется от NetRequest и добавляет status.
- *.
- * Формат JSON (response):
- * {
- * "op": "...",
- * "requestId": "...",
- * "status": 200,
- * "payload": { ... } // и для успеха, и для ошибки
- * }
- */
-public abstract class Net_Response extends Net_Request {
-
- /** Статус результата (200 — успех, любое другое значение — ошибка). */
- private int status;
-
- // --- getters / setters ---
-
- public int getStatus() {
- return status;
- }
-
- public void setStatus(int status) {
- this.status = status;
- }
-
- public boolean isOk() {
- return status == 200;
- }
-}
-
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 авторизации: запрос выдачи одноразового nonce (authNonce).
- *
- * Клиент по логину просит сервер сгенерировать случайный authNonce,
- * который будет использован на втором шаге при подписи.
- *
- * Формат входящего JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "payload": {
- * "login": "someLogin"
- * }
- * }
- *
- * Формат успешного ответа:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Request extends Net_Request {
-
- /**
- * Логин пользователя, для которого запускается авторизация.
- */
- private String login;
-
- public String getLogin() {
- return login;
- }
- public void setLogin(String login) {
- this.login = login;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на AuthChallenge.
- *
- * При успехе сервер возвращает одноразовый nonce для подписи (authNonce),
- * который клиент обязан использовать на втором шаге при формировании строки
- * для цифровой подписи.
- *
- * JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Response extends Net_Response {
-
- /**
- * Одноразовый nonce для авторификации.
- * Строка — это base64-представление 32 случайных байт.
- */
- private String authNonce;
-
- public String getAuthNonce() {
- return authNonce;
- }
-
- public void setAuthNonce(String authNonce) {
- this.authNonce = authNonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос CloseActiveSession — закрытие активной сессии пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и "AUTH_IN_PROGRESS" здесь больше нет.
- *
- * payload:
- * {
- * "sessionId": "..." // опционально; если пусто — закрываем текущую
- * }
- */
-public class Net_CloseActiveSession_Request extends Net_Request {
-
- /** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CloseActiveSession.
- *
- * При успехе:
- * - status = 200;
- * - payload = {}.
- *
- * Закрытие WebSocket-соединения может быть выполнено сразу (для другой сессии)
- * или чуть позже (для текущей сессии) после отправки ответа.
- */
-public class Net_CloseActiveSession_Response extends Net_Response {
- // Дополнительных полей пока не требуется.
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
- *
- * Шаги:
- * 1) AuthChallenge(login) -> authNonce
- * 2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
- *
- * Подпись deviceKey делается над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
- *
- * Важно:
- * - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
- * - В БД active_sessions.session_key хранится sessionPubKeyB64.
- */
-public class Net_CreateAuthSession_Request extends Net_Request {
-
- /** Клиентский пароль для хранения данных (base64 от 32 байт). */
- private String storagePwd;
-
- /** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
- private String sessionPubKeyB64;
-
- /** Время на стороне клиента (мс с 1970-01-01). */
- private long timeMs;
-
- /** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-
- public String getSessionPubKeyB64() {
- return sessionPubKeyB64;
- }
-
- public void setSessionPubKeyB64(String sessionPubKeyB64) {
- this.sessionPubKeyB64 = sessionPubKeyB64;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CreateAuthSession (v2).
- *
- * При успехе сервер создаёт запись в active_sessions
- * и возвращает идентификатор сессии sessionId.
- *
- * JSON:
- * {
- * "op": "CreateAuthSession",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "sessionId": "base64(32)"
- * }
- * }
- */
-public class Net_CreateAuthSession_Response extends Net_Response {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListSessions — список активных сессий пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Пустой payload.
- */
-public class Net_ListSessions_Request extends Net_Request {
- // пусто
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.List;
-
-/**
- * Ответ на ListSessions.
- *
- * При успехе:
- * - status = 200;
- * - payload:
- * {
- * "sessions": [
- * {
- * "sessionId": "...",
- * "clientInfoFromClient": "...",
- * "clientInfoFromRequest": "...",
- * "geo": "Country, City" | "unknown",
- * "lastAuthirificatedAtMs": 1733310000000
- * },
- * ...
- * ]
- * }
- */
-public class Net_ListSessions_Response extends Net_Response {
-
- /**
- * Список активных сессий для текущего пользователя.
- */
- private List sessions;
-
- public List getSessions() {
- return sessions;
- }
-
- public void setSessions(List sessions) {
- this.sessions = sessions;
- }
-
- /**
- * Описание одной активной сессии.
- */
- public static class SessionInfo {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- /** Что прислал клиент в CreateAuthSession/RefreshSession (clientInfo). */
- private String clientInfoFromClient;
-
- /** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
- private String clientInfoFromRequest;
-
- /** Строка геолокации вида "Country, City" или "unknown". */
- private String geo;
-
- /** Время последней успешной авторизации/refresh (мс с 1970-01-01). */
- private long lastAuthirificatedAtMs;
-
- // --- getters / setters ---
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- public String getClientInfoFromClient() {
- return clientInfoFromClient;
- }
-
- public void setClientInfoFromClient(String clientInfoFromClient) {
- this.clientInfoFromClient = clientInfoFromClient;
- }
-
- public String getClientInfoFromRequest() {
- return clientInfoFromRequest;
- }
-
- public void setClientInfoFromRequest(String clientInfoFromRequest) {
- this.clientInfoFromRequest = clientInfoFromRequest;
- }
-
- public String getGeo() {
- return geo;
- }
-
- public void setGeo(String geo) {
- this.geo = geo;
- }
-
- public long getLastAuthirificatedAtMs() {
- return lastAuthirificatedAtMs;
- }
-
- public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
- this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 входа в существующую сессию (v2):
- * SessionChallenge(sessionId) -> nonce
- */
-public class Net_SessionChallenge_Request extends Net_Request {
-
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionChallenge (v2).
- * payload: { "nonce": "base64(32)" }
- */
-public class Net_SessionChallenge_Response extends Net_Response {
-
- private String nonce;
-
- public String getNonce() {
- return nonce;
- }
-
- public void setNonce(String nonce) {
- this.nonce = nonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 входа в существующую сессию (v2):
- * SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
- *
- * Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- *
- * nonce берётся из SessionChallenge и хранится в ctx (одноразовый, TTL).
- */
-public class Net_SessionLogin_Request extends Net_Request {
-
- private String sessionId;
- private long timeMs;
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionLogin (v2).
- * payload: { "storagePwd": "base64(32)" }
- */
-public class Net_SessionLogin_Response extends Net_Response {
-
- private String storagePwd;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.security.SecureRandom;
-
-/**
- * AuthChallenge (v2) — шаг 1 создания новой сессии.
- *
- * Логика авторизации (v2):
- * - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
- * - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
- * CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
- *
- * Что делает:
- * 1) Проверяет login.
- * 2) Находит пользователя (solana_users).
- * 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
- * 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
- */
-public class Net_AuthChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_AuthChallenge_Request req = (Net_AuthChallenge_Request) baseReq;
-
- String login = req.getLogin();
- if (login == null || login.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_LOGIN",
- "Пустой логин"
- );
- }
-
- // Если по этому соединению уже есть залогиненный пользователь — не даём повторную авторификацию
- if (ctx.getLogin() != null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "ALREADY_AUTHED",
- "Попытка повторной авторификации для уже заданного login=" + ctx.getLogin()
- );
- }
-
- SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
- if (solanaUserEntry == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "UNKNOWN_USER",
- "Пользователь с таким логином не найден"
- );
- }
-
- ctx.setSolanaUser(solanaUserEntry);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String authNonce = Base64Ws.encode(buf);
-
- ctx.setAuthNonce(authNonce);
-
- Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setAuthNonce(authNonce);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-/**
- * CloseActiveSession (v2) — закрытие текущей или другой сессии.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и AUTH_IN_PROGRESS здесь больше нет.
- *
- * Закрытие:
- * - удаляем запись из БД
- * - если по sessionId есть активный WS — закрываем его
- */
-public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- String targetSessionId = req.getSessionId();
- if (targetSessionId == null || targetSessionId.isBlank()) {
- if (ctx.getSessionId() != null && !ctx.getSessionId().isBlank()) {
- targetSessionId = ctx.getSessionId();
- } else if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
- targetSessionId = ctx.getActiveSession().getSessionId();
- } else {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_SESSION_TO_CLOSE",
- "Не удалось определить, какую сессию нужно закрыть"
- );
- }
- }
-
- ActiveSessionEntry targetSession;
- try {
- targetSession = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных при поиске сессии"
- );
- }
-
- if (targetSession == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия для закрытия не найдена"
- );
- }
-
- if (currentLogin == null || !currentLogin.equals(targetSession.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_OF_ANOTHER_USER",
- "Нельзя закрывать сессию другого пользователя"
- );
- }
-
- boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
-
- closeActiveSession(targetSessionId, ctx, isCurrentSession);
-
- Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- return resp;
- }
-
- private void closeActiveSession(String targetSessionId,
- ConnectionContext currentCtx,
- boolean isCurrentSession) {
-
- try {
- ActiveSessionsDAO.getInstance().deleteBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
- }
-
- ConnectionContext ctxToClose =
- ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
-
- if (ctxToClose == null) return;
-
- if (isCurrentSession && ctxToClose == currentCtx) {
- new Thread(() -> {
- try { Thread.sleep(50); } catch (InterruptedException ignored) {}
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }, "CloseSession-" + targetSessionId).start();
- } else {
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import org.eclipse.jetty.websocket.api.Session;
-
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.sql.SQLException;
-
-/**
- * CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
- *
- * Логика авторизации (v2):
- * - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
- * - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
- * отправляет на сервер ТОЛЬКО sessionPubKeyB64.
- * - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
- *
- * Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
- *
- * На выходе:
- * - создаётся запись active_sessions
- * - ctx становится AUTH_STATUS_USER (вход выполнен как "текущая сессия")
- * - ответ: sessionId
- */
-public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
- private static final SecureRandom RANDOM = new SecureRandom();
-
- public static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
-
- if (ctx == null
- || ctx.getSolanaUser() == null
- || ctx.getAuthNonce() == null
- || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
-
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_STEP1_CONTEXT",
- "Шаг 1 авторизации не был корректно выполнен для данного соединения"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no step1 context or bad auth state");
- return err;
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String login = user.getLogin();
- if (login == null || login.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_LOGIN",
- "Для пользователя не задан login в БД"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no login");
- return err;
- }
-
- String storagePwd = req.getStoragePwd();
- if (storagePwd == null || storagePwd.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_STORAGE_PWD",
- "Пустой storagePwd"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty storagePwd");
- return err;
- }
-
- String sessionPubKeyB64 = req.getSessionPubKeyB64();
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_PUBKEY",
- "Пустой sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
- return err;
- }
-
- // Проверим, что sessionPubKeyB64 декодируется в 32 байта
- byte[] sessionPubKey32;
- try {
- sessionPubKey32 = Base64Ws.decode(sessionPubKeyB64);
- } catch (IllegalArgumentException e) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный base64 в sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
- return err;
- }
- if (sessionPubKey32.length != 32) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_SESSION_PUBKEY_LEN",
- "sessionPubKey должен быть 32 байта"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
- return err;
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая цифровая подпись"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty signature");
- return err;
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- long diff = Math.abs(nowMs - timeMs);
- if (diff > ALLOWED_SKEW_MS) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: time skew");
- return err;
- }
-
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String devicePubKeyB64 = user.getDeviceKey();
- if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_DEVICE_KEY",
- "Отсутствует deviceKey у пользователя"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no deviceKey");
- return err;
- }
-
- String authNonce = ctx.getAuthNonce();
-
- boolean sigOk;
- try {
- sigOk = verifyCreateSessionSignature(
- user,
- login,
- authNonce,
- timeMs,
- signatureB64
- );
- } catch (IllegalArgumentException ex) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный формат Base64 для ключа или подписи"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad base64");
- return err;
- }
-
- if (!sigOk) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad signature");
- return err;
- }
-
- // --- генерируем sessionId ---
- String sessionId = generateRandom32B64Url();
- long now = System.currentTimeMillis();
-
- // --- Сбор данных о клиенте (IP, UA, язык) ---
- Session wsSession = ctx.getWsSession();
- String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
- String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
-
- String clientIp = "";
- if (wsSession != null) {
- String ip = ClientInfoService.extractClientIp(wsSession);
- if (ip != null) clientIp = ip;
-
- if (!clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- // --- создаём запись ActiveSession и сохраняем в БД ---
- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
- ActiveSessionEntry activeSessionEntry;
-
- try {
- activeSessionEntry = new ActiveSessionEntry(
- sessionId,
- login,
- sessionPubKeyB64, // session_key (pubkey)
- storagePwd,
- now,
- now,
- null, // pushEndpoint
- null, // pushP256dhKey
- null, // pushAuthKey
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
-
- dao.insert(activeSessionEntry);
- } catch (SQLException e) {
- log.error("Ошибка БД при создании новой сессии для login={}", login, e);
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_SESSION_CREATE",
- "Ошибка БД при создании сессии"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db error");
- return err;
- }
-
- // --- обновляем контекст ---
- ctx.setActiveSession(activeSessionEntry);
- ctx.setSessionId(sessionId);
- ctx.setAuthNonce(null);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // --- формируем ответ ---
- Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessionId(sessionId);
- return resp;
- }
-
- private static boolean verifyCreateSessionSignature(
- SolanaUserEntry user,
- String login,
- String authNonce,
- long timeMs,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // deviceKey (pub, 32)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
- byte[] signature64 = Base64Ws.decode(signatureB64);
-
- String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-
- private static String generateRandom32B64Url() {
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- return Base64Ws.encode(buf);
- }
-}
-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.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.GeoLookupService;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListSessions (v2) — список активных сессий.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей здесь больше нет.
- */
-public class Net_ListSessions_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListSessions_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- List sessions;
- try {
- sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
- } catch (SQLException e) {
- log.error("Ошибка БД при получении списка сессий для login={}", currentLogin, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_LIST_SESSIONS",
- "Ошибка доступа к базе данных при получении списка сессий"
- );
- }
-
- List resultList = new ArrayList<>();
- for (ActiveSessionEntry s : sessions) {
- SessionInfo info = new SessionInfo();
- info.setSessionId(s.getSessionId());
- info.setClientInfoFromClient(s.getClientInfoFromClient());
- info.setClientInfoFromRequest(s.getClientInfoFromRequest());
- info.setLastAuthirificatedAtMs(s.getLastAuthirificatedAtMs());
-
- String ip = s.getClientIp();
- String geo = GeoLookupService.resolveCountryCityOrIpWithCache(ip);
- info.setGeo(geo);
-
- resultList.add(info);
- }
-
- Net_ListSessions_Response resp = new Net_ListSessions_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessions(resultList);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-
-import java.security.SecureRandom;
-import java.sql.SQLException;
-
-/**
- * SessionChallenge (v2) — шаг 1 входа в существующую сессию.
- *
- * Логика авторизации (v2):
- * - Вход в существующую сессию ВСЕГДА в 2 шага:
- * 1) SessionChallenge(sessionId) -> nonce
- * 2) SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
- *
- * Что делает:
- * - Проверяет, что sessionId существует в БД.
- * - Генерирует одноразовый nonce (base64url(32)), сохраняет его в ctx:
- * ctx.sessionLoginNonce, ctx.sessionLoginSessionId, ctx.sessionLoginNonceExpiresAtMs.
- */
-public class Net_SessionChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
- private static final long NONCE_TTL_MS = 60_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionChallenge_Request req = (Net_SessionChallenge_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String nonce = Base64Ws.encode(buf);
-
- long now = System.currentTimeMillis();
- ctx.setSessionLoginNonce(nonce);
- ctx.setSessionLoginSessionId(sessionId);
- ctx.setSessionLoginNonceExpiresAtMs(now + NONCE_TTL_MS);
-
- Net_SessionChallenge_Response resp = new Net_SessionChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setNonce(nonce);
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.SQLException;
-
-/**
- * SessionLogin (v2) — шаг 2 входа в существующую сессию (по sessionKey).
- *
- * Логика авторизации (v2):
- * - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
- * - SessionLogin проверяет подпись sessionKey над строкой:
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- * - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
- *
- * При успехе:
- * - ctx становится AUTH_STATUS_USER
- * - обновляем метаданные сессии (lastAuth + clientIp + clientInfo + lang)
- * - возвращаем storagePwd
- */
-public class Net_SessionLogin_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
-
- private static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- // проверка челленджа
- if (ctx.getSessionLoginNonce() == null
- || ctx.getSessionLoginSessionId() == null
- || System.currentTimeMillis() > ctx.getSessionLoginNonceExpiresAtMs()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_CHALLENGE",
- "Нет активного SessionChallenge или nonce истёк"
- );
- }
-
- if (!sessionId.equals(ctx.getSessionLoginSessionId())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "SESSION_ID_MISMATCH",
- "nonce был выдан для другого sessionId"
- );
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая подпись"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- String sessionPubKeyB64 = session.getSessionKey(); // это pubKey (Base64(32))
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_SESSION_KEY",
- "В сессии не задан session_key"
- );
- }
-
- String nonce = ctx.getSessionLoginNonce();
-
- boolean sigOk;
- try {
- sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный Base64 для ключа/подписи"
- );
- }
-
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- }
-
- // сжигаем nonce
- ctx.setSessionLoginNonce(null);
- ctx.setSessionLoginSessionId(null);
- ctx.setSessionLoginNonceExpiresAtMs(0);
-
- // подтягиваем пользователя
- SolanaUserEntry user;
- try {
- user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_USER_LOOKUP",
- "Ошибка доступа к базе данных при получении пользователя"
- );
- }
-
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "USER_NOT_FOUND_FOR_SESSION",
- "Пользователь для данной сессии не найден"
- );
- }
-
- // обновление метаданных
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String clientIp = null;
- String clientInfoFromRequest = null;
- String userLanguage = null;
-
- if (ctx.getWsSession() != null) {
- clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
- clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
- userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
-
- if (clientIp != null && !clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- long now = System.currentTimeMillis();
- try {
- ActiveSessionsDAO.getInstance().updateOnRefresh(
- sessionId,
- now,
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
- } catch (SQLException e) {
- log.error("Ошибка БД при updateOnRefresh sessionId={}", sessionId, e);
- }
-
- session.setLastAuthirificatedAtMs(now);
- session.setClientIp(clientIp);
- session.setClientInfoFromClient(clientInfoFromClient);
- session.setClientInfoFromRequest(clientInfoFromRequest);
- session.setUserLanguage(userLanguage);
-
- // ctx
- ctx.setActiveSession(session);
- ctx.setSolanaUser(user);
- ctx.setSessionId(sessionId);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // ответ
- Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setStoragePwd(session.getStoragePwd());
- return resp;
- }
-
- private static boolean verifySessionLoginSignature(
- String sessionPubKeyB64,
- String sessionId,
- long timeMs,
- String nonce,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // pubKey: Base64(32). (Ed25519Util.keyFromBase64 должен использовать стандартный Base64)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);
-
- // signature: Base64(64) через единую утилиту WS-протокола
- byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
-
- String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-public final class Net_AddBlock_Request extends Net_Request {
-
- private String blockchainName; // обязателен
- private int blockNumber; // обязателен
- private String prevBlockHash; // HEX(64) или "" для нулевого
- private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public int getBlockNumber() { return blockNumber; }
- public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; }
-
- public String getPrevBlockHash() { return prevBlockHash; }
- public void setPrevBlockHash(String prevBlockHash) { this.prevBlockHash = prevBlockHash; }
-
- public String getBlockBytesB64() { return blockBytesB64; }
- public void setBlockBytesB64(String blockBytesB64) { this.blockBytesB64 = blockBytesB64; }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ:
- * - reasonCode (null если ok)
- * - serverLastGlobalNumber / serverLastGlobalHash
- */
-public final class Net_AddBlock_Response extends Net_Response {
-
- /** null если ok, иначе строка причины (bad_block_base64, user_not_found, и т.п.) */
- private String reasonCode;
-
- /** что сервер считает последним по глобальной цепочке */
- private int serverLastGlobalNumber;
- private String serverLastGlobalHash;
-
- public String getReasonCode() { return reasonCode; }
- public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
-
- 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; }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain;
-
-import blockchain.BchBlockEntry;
-import blockchain.BchCryptoVerifier;
-import blockchain.MsgSubType;
-import blockchain.body.BodyHasLine;
-import blockchain.body.BodyHasTarget;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainLocks;
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.BlocksDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.BlockEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.util.Arrays;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * Net_AddBlock_Handler — единый хэндлер добавления блока (JSON).
- *
- * Новый порядок валидации (ТЗ):
- * 1) Достаём из blockchain_state: last_block_number, last_block_hash
- * 2) Проверяем:
- * - incoming.blockNumber == last+1
- * - incoming.prevHash32 == last_hash (для genesis last_hash = 32 нулей)
- * 3) Проверяем подпись Ed25519.verify(hash32(preimage), signature64, pubKey)
- * 4) Если тип имеет линию:
- * - если prevLineNumber != null:
- * достаём hash блока prevLineNumber из blocks
- * сравниваем с prevLineHash32 из body
- * 5) Сохраняем блок в blocks + обновляем blockchain_state
- *
- * Важно:
- * - Сетевой протокол AddBlock пока оставляем старые поля (globalNumber/prevGlobalHash),
- * но внутренняя логика использует НОВЫЙ формат блока.
- */
-public final class Net_AddBlock_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_Handler.class);
-
- private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
- private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) {
-
- Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;
-
- String blockchainName = req.getBlockchainName();
- ReentrantLock lock = BlockchainLocks.lockFor(blockchainName);
- lock.lock();
- try {
- AddBlockResult r = addBlock(
- blockchainName,
- req.getBlockNumber(), // старое поле, пока оставляем
- req.getPrevBlockHash(), // старое поле, пока оставляем
- req.getBlockBytesB64()
- );
-
- Net_AddBlock_Response resp = new Net_AddBlock_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
-
- if (r.isOk()) {
- resp.setStatus(WireCodes.Status.OK);
- resp.setReasonCode(null);
- } else {
- resp.setStatus(r.httpStatus);
- resp.setReasonCode(r.reasonCode);
- }
-
- resp.setServerLastGlobalNumber(r.serverLastBlockNumber);
- resp.setServerLastGlobalHash(r.serverLastBlockHashHex);
-
- return resp;
-
- } finally {
- lock.unlock();
- }
- }
-
- private AddBlockResult addBlock(
- String blockchainName,
- int globalNumberFromReq,
- String prevGlobalHashHexFromReq,
- String blockBytesB64
- ) {
- if (blockchainName == null || blockchainName.isBlank()) {
- log.warn("AddBlock: пустой blockchainName (reqGlobalNumber={})", globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
- }
-
- String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
- if (login == null || login.isBlank()) {
- log.warn("AddBlock: плохой blockchainName='{}' => login не получился (reqGlobalNumber={})",
- blockchainName, globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
- }
-
- // 1) state обязателен
- final BlockchainStateEntry st;
- try {
- st = stateDAO.getByBlockchainName(blockchainName);
- } catch (Exception e) {
- log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
- }
-
- if (st == null) {
- log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
- }
-
- final int serverLastNum = st.getLastBlockNumber();
- final byte[] serverLastHash32 = (serverLastNum < 0)
- ? new byte[32]
- : require32OrThrow(st.getLastBlockHash(), "state.last_block_hash is null/invalid");
-
- final String serverLastHashHex = toHex(serverLastHash32);
-
- // 2) decode block
- final byte[] blockBytes;
- try {
- blockBytes = decodeBase64(blockBytesB64);
- } catch (Exception e) {
- log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
- }
-
- // 3) лимит (оставляем как было)
- try {
- long oldSize = st.getFileSizeBytes();
- long limit = st.getSizeLimit();
- long newSize = safeAdd(oldSize, blockBytes.length);
-
- if (limit > 0 && newSize > limit) {
- log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, oldSize={}, addLen={}, newSize={}, limit={})",
- login, blockchainName, oldSize, blockBytes.length, newSize, limit);
- return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
- }
- } catch (Exception e) {
- log.error("AddBlock: limit_check_failed (login={}, blockchainName={})", login, blockchainName, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
- }
-
- // 4) parse block
- final BchBlockEntry block;
- try {
- block = new BchBlockEntry(blockBytes);
- } catch (Exception e) {
- log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, bytesLen={})",
- login, blockchainName, blockBytes.length, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
- }
-
- // body.check()
- try {
- block.body.check();
- } catch (Exception e) {
- log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, blockNumber={}, type={}, ver={})",
- login, blockchainName, block.blockNumber, (block.type & 0xFFFF), (block.version & 0xFFFF), e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
- }
-
- // 4.2) запрет дырок: blockNumber строго last+1
- int expectedBlockNumber = serverLastNum + 1;
- if (block.blockNumber != expectedBlockNumber) {
- log.warn("AddBlock: bad_block_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={})",
- login, blockchainName, block.blockNumber, expectedBlockNumber, serverLastNum);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_number", serverLastNum, serverLastHashHex);
- }
-
- // (временная совместимость) req.globalNumber должен совпасть с block.blockNumber
- if (globalNumberFromReq != block.blockNumber) {
- log.warn("AddBlock: req_global_mismatch (login={}, blockchainName={}, reqGlobal={}, blockNumber={})",
- login, blockchainName, globalNumberFromReq, block.blockNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "req_global_mismatch", serverLastNum, serverLastHashHex);
- }
-
- // 4.3) проверка цепочки по prevHash32
- if (!Arrays.equals(block.prevHash32, serverLastHash32)) {
- log.warn("AddBlock: bad_prev_hash (login={}, blockchainName={}, blockNumber={}, clientPrev={}, serverPrev={})",
- login, blockchainName, block.blockNumber, toHex(block.prevHash32), serverLastHashHex);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_hash", serverLastNum, serverLastHashHex);
- }
-
- // 5) pubKey
- final byte[] pubKey32 = st.getBlockchainKeyBytes();
- if (pubKey32 == null || pubKey32.length != 32) {
- log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, blockNumber={}, keyLen={})",
- login, blockchainName, block.blockNumber, (pubKey32 == null ? -1 : pubKey32.length));
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
- }
-
- // 6) подпись по hash32(preimage)
- boolean sigOk;
- try {
- sigOk = BchCryptoVerifier.verifyBlock(block, pubKey32);
- } catch (Exception e) {
- log.warn("AddBlock: signature_verify_failed (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
- }
-
- if (!sigOk) {
- log.warn("AddBlock: bad_signature (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
- }
-
- // 7) line columns (only for BodyHasLine)
- Integer lineCode = null;
- Integer prevLineNumber = null;
- byte[] prevLineHash32 = null;
- Integer thisLineNumber = null;
-
- if (block.body instanceof BodyHasLine bl) {
- lineCode = bl.lineCode();
- prevLineNumber = bl.prevLineBlockGlobalNumber();
- prevLineHash32 = bl.prevLineBlockHash32();
- thisLineNumber = bl.lineSeq();
-
- // Нормализация: -1 не пишем в БД (для совместимости со старым TextBody)
- if (prevLineNumber != null && prevLineNumber == -1) {
- prevLineNumber = null;
- prevLineHash32 = null;
- thisLineNumber = null;
- }
-
- // Если prevLineNumber задан — проверяем его хэш
- if (prevLineNumber != null) {
- try {
- byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
- if (dbPrevHash == null) {
- log.warn("AddBlock: prev_line_block_not_found (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
- login, blockchainName, block.blockNumber, prevLineNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_line_block_not_found", serverLastNum, serverLastHashHex);
- }
- if (!Arrays.equals(dbPrevHash, require32OrThrow(prevLineHash32, "prevLineHash32 invalid"))) {
- log.warn("AddBlock: bad_prev_line_hash (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
- login, blockchainName, block.blockNumber, prevLineNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_line_hash", serverLastNum, serverLastHashHex);
- }
- } catch (Exception e) {
- log.error("AddBlock: db_error_prev_line_check (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_prev_line_check", serverLastNum, serverLastHashHex);
- }
- }
- }
-
- // 8) сформировать запись и записать (DB + state + файл)
- try {
- BlockEntry be = new BlockEntry();
- be.setLogin(login);
- be.setBchName(blockchainName);
-
- be.setBlockNumber(block.blockNumber);
- be.setMsgType(block.type & 0xFFFF);
- be.setMsgSubType(block.subType & 0xFFFF);
-
- be.setBlockBytes(block.toBytes());
- be.setBlockHash(block.getHash32());
- be.setBlockSignature(block.getSignature64());
-
- // line columns (optional)
- be.setLineCode(lineCode);
- be.setPrevLineNumber(prevLineNumber);
- be.setPrevLineHash(prevLineHash32);
- be.setThisLineNumber(thisLineNumber);
-
- // target columns (optional)
- if (block.body instanceof BodyHasTarget t) {
- be.setToLogin(t.toLogin());
- be.setToBchName(t.toBchName());
- be.setToBlockNumber(t.toBlockGlobalNumber());
- be.setToBlockHash(t.toBlockHashBytes());
- }
-
- // edit helper (optional): если TEXT_EDIT_* — это "редактирование блока цели"
- int type = block.type & 0xFFFF;
- int sub = block.subType & 0xFFFF;
-
- if (type == 1
- && (sub == (MsgSubType.TEXT_EDIT_POST & 0xFFFF) || sub == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF))
- && be.getToBlockNumber() != null) {
- be.setEditedByBlockNumber(be.getToBlockNumber());
- }
-
- dbWriter.appendBlockAndState(blockchainName, block, st, be);
-
- } catch (Exception e) {
- log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
- }
-
- String newHashHex = toHex(block.getHash32());
-
- log.info("✅ AddBlock ok: login={}, blockchainName={}, blockNumber={}, newHash={}",
- login, blockchainName, block.blockNumber, newHashHex);
-
- return new AddBlockResult(WireCodes.Status.OK, null, block.blockNumber, newHashHex);
- }
-
- /* ===================================================================== */
- /* ====================== Helpers ====================================== */
- /* ===================================================================== */
-
- private static byte[] decodeBase64(String b64) {
- if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
- return Base64Ws.decode(b64);
- }
-
- private static long safeAdd(long a, long b) {
- long r = a + b;
- if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
- return r;
- }
-
- private static byte[] require32OrThrow(byte[] b, String msg) {
- if (b == null || b.length != 32) throw new IllegalArgumentException(msg);
- return b;
- }
-
- private static String toHex(byte[] bytes) {
- if (bytes == null) return "null";
- char[] HEX = "0123456789abcdef".toCharArray();
- char[] out = new char[bytes.length * 2];
- for (int i = 0; i < bytes.length; i++) {
- int v = bytes[i] & 0xFF;
- out[i * 2] = HEX[v >>> 4];
- out[i * 2 + 1] = HEX[v & 0x0F];
- }
- return new String(out);
- }
-
- private static final class AddBlockResult {
- final int httpStatus;
- final String reasonCode;
- final int serverLastBlockNumber;
- final String serverLastBlockHashHex;
-
- AddBlockResult(int httpStatus, String reasonCode, int serverLastBlockNumber, String serverLastBlockHashHex) {
- this.httpStatus = httpStatus;
- this.reasonCode = reasonCode;
- this.serverLastBlockNumber = serverLastBlockNumber;
- this.serverLastBlockHashHex = serverLastBlockHashHex;
- }
-
- boolean isOk() { return httpStatus == WireCodes.Status.OK; }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.ReentrantLock;
-
-public final class BlockchainLocks {
- private static final ConcurrentHashMap MAP = new ConcurrentHashMap<>();
-
- private BlockchainLocks() {}
-
- public static ReentrantLock lockFor(String blockchainName) {
- return MAP.computeIfAbsent(blockchainName, id -> new ReentrantLock(true)); // fair=true
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
-
-import blockchain.BchBlockEntry;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.BlocksDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.BlockEntry;
-import utils.files.FileStoreUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-/**
- * BlockchainWriter — запись блока в DB + обновление state + запись в файл.
- *
- * ВАЖНО:
- * - Это минимальный рабочий вариант под новый формат.
- * - Если у тебя уже есть "атомарность" сложнее (tmp_bch + commit/recovery) — можно усилить потом.
- */
-public final class BlockchainWriter {
-
- private final BlocksDAO blocksDAO;
- private final BlockchainStateDAO stateDAO;
- private final FileStoreUtil fs = FileStoreUtil.getInstance();
-
- public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
- this.blocksDAO = blocksDAO;
- this.stateDAO = stateDAO;
- }
-
- public void appendBlockAndState(String blockchainName,
- BchBlockEntry block,
- BlockchainStateEntry st,
- BlockEntry be) throws SQLException {
-
- long nowMs = System.currentTimeMillis();
-
- try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
- c.setAutoCommit(false);
- try {
- // 1) insert block
- blocksDAO.insert(c, be);
-
- // 2) update state
- st.setLastBlockNumber(block.blockNumber);
- st.setLastBlockHash(block.getHash32());
- st.setFileSizeBytes(st.getFileSizeBytes() + block.toBytes().length);
- st.setUpdatedAtMs(nowMs);
-
- stateDAO.upsert(c, st);
-
- c.commit();
- } catch (Exception e) {
- try { c.rollback(); } catch (Exception ignored) {}
- if (e instanceof SQLException se) throw se;
- throw new SQLException("appendBlockAndState failed", e);
- } finally {
- try { c.setAutoCommit(true); } catch (Exception ignored) {}
- }
- }
-
- // 3) append to file (минимально: просто дописать)
- // Если у тебя уже есть логика tmp_bch+atomicReplace — можно заменить тут.
- String fileName = fs.buildBlockchainFileName(blockchainName);
- fs.addDataToFile(fileName, block.toBytes());
- }
-}
-package server.logic.ws_protocol.JSON.handlers.connections.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetFriendsLists — получить два списка "друзей" по connections_state.
- *
- * {
- * "op": "GetFriendsLists",
- * "requestId": "req-100",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * Возвращает:
- * - out_friends: кому login поставил FRIEND
- * - in_friends: кто поставил FRIEND этому login
- *
- * ПРО ДОСТУП (на будущее):
- * Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей.
- */
-public class Net_GetFriendsLists_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.connections.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ GetFriendsLists.
- *
- * {
- * "op": "GetFriendsLists",
- * "requestId": "req-100",
- * "status": 200,
- * "payload": {
- * "login": "Anya", // канонический регистр из БД
- * "out_friends": ["Bob", "Kate"], // кому login поставил FRIEND
- * "in_friends": ["Alex", "Kate"] // кто поставил FRIEND login
- * }
- * }
- */
-public class Net_GetFriendsLists_Response extends Net_Response {
-
- private String login;
-
- private List out_friends = new ArrayList<>();
- private List in_friends = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List getOut_friends() { return out_friends; }
- public void setOut_friends(List out_friends) { this.out_friends = out_friends; }
-
- public List getIn_friends() { return in_friends; }
- public void setIn_friends(List in_friends) { this.in_friends = in_friends; }
-}
-package server.logic.ws_protocol.JSON.handlers.connections;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.MsgSubType;
-import shine.db.SqliteDbController;
-import shine.db.dao.ConnectionsStateDAO;
-
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.util.List;
-
-/**
- * GetFriendsLists — получить 2 списка:
- * - out_friends: кому login поставил FRIEND
- * - in_friends: кто поставил FRIEND этому login
- *
- * ВАЖНО:
- * - login в запросе может быть любым регистром
- * - в ответе возвращаем канонический регистр (как в solana_users.login)
- *
- * ПРИМЕЧАНИЕ:
- * Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
- */
-public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- final String loginAnyCase = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
-
- // 1) Канонизируем login через solana_users (NOCASE)
- String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
- if (canonicalLogin == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- int relType = (int) MsgSubType.CONNECTION_FRIEND;
-
- // 2) Два списка (логины канонические)
- List outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType);
- List inFriends = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType);
-
- Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(canonicalLogin);
- resp.setOut_friends(outFriends);
- resp.setIn_friends(inFriends);
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetFriendsLists", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-
- private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
- String sql = """
- SELECT login
- FROM solana_users
- WHERE login = ? COLLATE NOCASE
- LIMIT 1
- """;
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, loginAnyCase);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return rs.getString("login");
- }
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers;
-
-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;
-
-/**
- * Общий интерфейс для всех JSON-хэндлеров.
- */
-public interface JsonMessageHandler {
-
- /**
- * Обработать запрос и вернуть ответ.
- *
- * @param request распарсенный запрос
- * @param ctx контекст текущего WebSocket-соединения
- */
- Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception;
-}
-
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос AddUser — временная/тестовая регистрация локального пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "payload": {
- * "login": "anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "base64-ed25519-public-key-login",
- * "blockchainKey": "base64-ed25519-public-key-blockchain",
- * "deviceKey": "base64-ed25519-public-key-device",
- * "bchLimit": 1000000
- * }
- * }
- *
- * Все поля лежат внутри payload.
- */
-public class Net_AddUser_Request extends Net_Request {
-
- private String login;
- private String blockchainName;
-
- /** Ключ пользователя Solana (публичный ключ логина) */
- private String solanaKey;
-
- /** Ключ блокчейна (публичный ключ блокчейна) */
- private String blockchainKey;
-
- /** Ключ устройства (публичный ключ устройства) */
- private String deviceKey;
-
- private Integer bchLimit;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-
- public Integer getBchLimit() { return bchLimit; }
- public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
-}
-// file: server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Успешный ответ на AddUser.
- *
- * Сейчас дополнительных полей нет — достаточно status=200.
- *
- * Пример:
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_AddUser_Response extends Net_Response {
- // При необходимости сюда можно добавить, например, флаг created/updated и т.п.
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUser — проверка/получение пользователя по login.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "payload": {
- * "login": "AnYa"
- * }
- * }
- *
- * Поиск по login выполняется без учёта регистра.
- * В ответе возвращаем login/blockchainName с тем регистром, как в БД.
- */
-public class Net_GetUser_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUser.
- *
- * Всегда status=200.
- *
- * Пример (нет пользователя):
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "status": 200,
- * "payload": { "exists": false }
- * }
- *
- * Пример (есть пользователь):
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "status": 200,
- * "payload": {
- * "exists": true,
- * "login": "Anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "...",
- * "blockchainKey": "...",
- * "deviceKey": "..."
- * }
- * }
- */
-public class Net_GetUser_Response extends Net_Response {
-
- private Boolean exists;
-
- private String login;
- private String blockchainName;
- private String solanaKey;
- private String blockchainKey;
- private String deviceKey;
-
- public Boolean getExists() { return exists; }
- public void setExists(Boolean exists) { this.exists = exists; }
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос SearchUsers — поиск логинов по префиксу.
- *
- * Клиент отправляет:
- * {
- * "op": "SearchUsers",
- * "requestId": "su-1",
- * "payload": { "prefix": "any" }
- * }
- *
- * Поиск по prefix выполняется без учёта регистра.
- * В ответе возвращаем логины с тем регистром, как в БД.
- */
-public class Net_SearchUsers_Request extends Net_Request {
-
- private String prefix;
-
- public String getPrefix() { return prefix; }
- public void setPrefix(String prefix) { this.prefix = prefix; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ SearchUsers.
- *
- * Всегда status=200.
- *
- * Пример:
- * {
- * "op": "SearchUsers",
- * "requestId": "su-1",
- * "status": 200,
- * "payload": {
- * "logins": ["Anya", "andrew", "Angel"]
- * }
- * }
- */
-public class Net_SearchUsers_Response extends Net_Response {
-
- private List logins = new ArrayList<>();
-
- public List getLogins() { return logins; }
- public void setLogins(List logins) { this.logins = logins; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.SolanaUserEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-public class Net_AddUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
-
- /** TEST ONLY */
- private static final int TEST_BCH_LIMIT = 1_000_000;
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getBlockchainName() == null || req.getBlockchainName().isBlank()
- || req.getSolanaKey() == null || req.getSolanaKey().isBlank()
- || req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
- || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
- );
- }
-
- // blockchainName должен быть вида: -NNN
- if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BLOCKCHAIN_NAME",
- "blockchainName должен быть вида -NNN (пример: anya-001)"
- );
- }
-
- int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0)
- ? TEST_BCH_LIMIT
- : req.getBchLimit();
-
- try {
- // базовая валидация форматов ключей: Base64(32 bytes)
- byte[] solanaKey32;
- byte[] blockchainKey32;
- byte[] deviceKey32;
-
- try {
- solanaKey32 = Base64Ws.decodeLen(req.getSolanaKey(), 32, "solanaKey");
- blockchainKey32 = Base64Ws.decodeLen(req.getBlockchainKey(), 32, "blockchainKey");
- deviceKey32 = Base64Ws.decodeLen(req.getDeviceKey(), 32, "deviceKey");
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_KEY_FORMAT",
- e.getMessage()
- );
- }
-
- // (переменные не используются дальше, но оставляем для ясности проверки длины)
- if (solanaKey32.length != 32 || blockchainKey32.length != 32 || deviceKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_KEY_FORMAT",
- "solanaKey/blockchainKey/deviceKey должны быть Base64(32 bytes)"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- SqliteDbController db = SqliteDbController.getInstance();
-
- try (Connection c = db.getConnection()) {
- c.setAutoCommit(false);
-
- // 1. Проверяем, что пользователя нет (case-insensitive)
- if (usersDAO.getByLogin(c, req.getLogin()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "USER_ALREADY_EXISTS",
- "Пользователь с таким login уже существует"
- );
- }
-
- // 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД)
- if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_ALREADY_EXISTS",
- "Пользователь с таким blockchainName уже существует"
- );
- }
-
- // 3. На всякий случай оставляем старую проверку blockchain_state,
- // потому что эта таблица нужна серверу (состояние цепочки/лимиты).
- if (stateDAO.getByBlockchainName(c, req.getBlockchainName()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_STATE_ALREADY_EXISTS",
- "blockchain_state уже существует"
- );
- }
-
- // 4. Создаём пользователя (все поля теперь лежат в solana_users)
- SolanaUserEntry user = new SolanaUserEntry();
- user.setLogin(req.getLogin());
- user.setBlockchainName(req.getBlockchainName());
- user.setSolanaKey(req.getSolanaKey());
- user.setBlockchainKey(req.getBlockchainKey());
- user.setDeviceKey(req.getDeviceKey());
-
- usersDAO.insert(c, user);
-
- // 5. Создаём INITIAL blockchain_state (для работы сервера)
- BlockchainStateEntry st = new BlockchainStateEntry();
- st.setBlockchainName(req.getBlockchainName());
- st.setLogin(req.getLogin());
- st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
- st.setLastBlockNumber(-1);
- st.setLastBlockHash(new byte[32]);
- st.setFileSizeBytes(0);
- st.setSizeLimit(limit);
- st.setUpdatedAtMs(System.currentTimeMillis());
-
- stateDAO.upsert(c, st);
-
- c.commit();
- }
-
- Net_AddUser_Response resp = new Net_AddUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
- req.getLogin(), req.getBlockchainName(), limit);
-
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-public class Net_GetUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUser_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUser_Request req = (Net_GetUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- // тут логичнее BAD_REQUEST, но ты просил: "нет пользователя" тоже 200.
- // Поэтому BAD_REQUEST оставляем только на реально пустой login.
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
-
- try {
- SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
-
- Net_GetUser_Response resp = new Net_GetUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (u == null) {
- resp.setExists(false);
- log.info("ℹ️ GetUser: not found for login={}", req.getLogin());
- return resp;
- }
-
- // ВАЖНО:
- // - Поиск по login был case-insensitive,
- // - а тут возвращаем login/blockchainName как в БД (с исходным регистром).
- resp.setExists(true);
- resp.setLogin(u.getLogin());
- resp.setBlockchainName(u.getBlockchainName());
- resp.setSolanaKey(u.getSolanaKey());
- resp.setBlockchainKey(u.getBlockchainKey());
- resp.setDeviceKey(u.getDeviceKey());
-
- log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName());
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error GetUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error GetUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class Net_SearchUsers_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SearchUsers_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_SearchUsers_Request req = (Net_SearchUsers_Request) baseRequest;
-
- if (req.getPrefix() == null || req.getPrefix().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: prefix"
- );
- }
-
- String prefix = req.getPrefix().trim();
-
- try {
- SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
- List users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5
-
- List logins = new ArrayList<>();
- for (SolanaUserEntry u : users) {
- if (u != null && u.getLogin() != null) {
- logins.add(u.getLogin()); // регистр как в БД
- }
- }
-
- Net_SearchUsers_Response resp = new Net_SearchUsers_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setLogins(logins);
-
- log.info("✅ SearchUsers ok: prefix='{}' -> {}", prefix, logins.size());
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error SearchUsers", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error SearchUsers", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUserParam — получить один параметр пользователя.
- *
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) этот запрос не ограничивает просмотр параметров, т.к. проект в тестовом режиме.
- * Позже, вероятно, потребуется ограничить: кто и какие параметры может читать (сессия/права).
- * Но для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUserParam.
- *
- * Если найден:
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * }
- * }
- *
- * Если не найден:
- * status=404, payload пустой.
- */
-public class Net_GetUserParam_Response extends Net_Response {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListUserParams — получить все сохранённые параметры пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ ListUserParams — список всех параметров пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "params": [
- * {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * },
- * ...
- * ]
- * }
- * }
- */
-public class Net_ListUserParams_Response extends Net_Response {
-
- private String login;
- private List
- params = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List
- getParams() { return params; }
- public void setParams(List
- params) { this.params = params; }
-
- public static class Item {
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-ed25519-public-key-32",
- * "signature": "base64-ed25519-signature-64"
- * }
- * }
- *
- * Подпись считается от UTF-8 строки:
- * USER_PARAMETER_PREFIX + login + param + time_ms + value
- */
-public class Net_UpsertUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
-
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на UpsertUserParam.
- *
- * Успех:
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_UpsertUserParam_Response extends Net_Response {
- // MVP: без payload. При желании позже можно добавить created/updated.
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-
-/**
- * GetUserParam — получить один параметр пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUserParam_Request req = (Net_GetUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param"
- );
- }
-
- String login = req.getLogin().trim();
- String param = req.getParam().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- UserParamEntry e = dao.getByLoginAndParam(c, login, param);
-
- if (e == null) {
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(404);
- return resp;
- }
-
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(e.getLogin());
- resp.setParam(e.getParam());
- resp.setTime_ms(e.getTimeMs());
- resp.setValue(e.getValue());
- resp.setDevice_key(e.getDeviceKey());
- resp.setSignature(e.getSignature());
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListUserParams — получить все параметры пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListUserParams_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_ListUserParams_Request req = (Net_ListUserParams_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- String login = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- List entries;
- try (Connection c = db.getConnection()) {
- entries = dao.getByLogin(c, login);
- }
-
- Net_ListUserParams_Response resp = new Net_ListUserParams_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(login);
-
- List items = new ArrayList<>();
- for (UserParamEntry e : entries) {
- Net_ListUserParams_Response.Item it = new Net_ListUserParams_Response.Item();
- it.setLogin(e.getLogin());
- it.setParam(e.getParam());
- it.setTime_ms(e.getTimeMs());
- it.setValue(e.getValue());
- it.setDevice_key(e.getDeviceKey());
- it.setSignature(e.getSignature());
- items.add(it);
- }
- resp.setParams(items);
-
- return resp;
-
- } catch (Exception e) {
- log.error("❌ Internal error ListUserParams", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.SolanaUserEntry;
-import shine.db.entities.UserParamEntry;
-import utils.config.ShineSignatureConstants;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.Connection;
-import java.sql.SQLException;
-
-/**
- * Net_UpsertUserParam_Handler
- *
- * Делает (MVP, без "сессий"):
- * 1) Проверка входных полей.
- * 2) Проверка подписи Ed25519 по device_key.
- * 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
- * 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
- *
- * ВАЖНО:
- * - НИКАКИХ ручных транзакций / BEGIN здесь нет.
- * - autoCommit=true, каждый statement завершённый сам по себе.
- * - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
- * наш финальный UPSERT просто вернёт 0 обновлённых строк.
- */
-public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()
- || req.getTime_ms() == null || req.getTime_ms() <= 0
- || req.getValue() == null
- || req.getDevice_key() == null || req.getDevice_key().isBlank()
- || req.getSignature() == null || req.getSignature().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param/time_ms/value/device_key/signature"
- );
- }
-
- final String login = req.getLogin().trim();
- final String param = req.getParam().trim();
- final long timeMs = req.getTime_ms();
- final String value = req.getValue();
- final String deviceKeyB64 = req.getDevice_key().trim();
- final String signatureB64 = req.getSignature().trim();
-
- try {
- // ---------------- Base64 decode ----------------
- byte[] pubKey32;
- byte[] sig64;
- try {
- pubKey32 = Base64Ws.decodeLen(deviceKeyB64, 32, "device_key");
- sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "device_key/signature должны быть Base64"
- );
- }
-
- // ---------------- Signature verify ----------------
- String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
- + login
- + param
- + timeMs
- + value;
-
- byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
-
- boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "SIGNATURE_INVALID",
- "Подпись не прошла проверку"
- );
- }
-
- // ---------------- DB checks + upsert ----------------
- SqliteDbController db = SqliteDbController.getInstance();
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- // 1) user exists
- SolanaUserEntry user = usersDAO.getByLogin(c, login);
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- // 2) device key must match the user's stored deviceKey
- String userDeviceKey = user.getDeviceKey();
- if (userDeviceKey == null || userDeviceKey.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "USER_DEVICE_KEY_EMPTY",
- "У пользователя не задан deviceKey в БД"
- );
- }
-
- if (!userDeviceKey.trim().equals(deviceKeyB64)) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "DEVICE_KEY_MISMATCH",
- "device_key не соответствует пользователю"
- );
- }
-
- // 3) atomic upsert-if-newer
- UserParamEntry e = new UserParamEntry(
- login,
- param,
- timeMs,
- value,
- deviceKeyB64,
- signatureB64
- );
-
- int changed = paramsDAO.upsertIfNewer(c, e);
-
- Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (changed == 1) {
- log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
- } else {
- // 0 строк — значит в БД уже есть time_ms >= incoming
- log.info("ℹ️ UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
- }
-
- return resp;
- }
-
- } catch (SQLException e) {
- log.error("❌ DB error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-
-import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
-
-// --- NEW v2 session login ---
-import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionChallenge_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionLogin_Handler;
-
-// --- auth entities ---
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
-
-// --- NEW v2 entities ---
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
-
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
-
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
-
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
-
-// --- NEW: SearchUsers ---
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_SearchUsers_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Request;
-
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_GetUserParam_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
-
-// --- subscriptions ---
-
-// --- NEW: connections friends lists ---
-import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
-
-import java.util.Map;
-
-/**
- * JsonHandlerRegistry — единое место, где руками регистрируются
- * JSON-операции: op → handler и op → requestClass.
- */
-public final class JsonHandlerRegistry {
-
- private static final Map HANDLERS = Map.ofEntries(
- Map.entry("AddUser", new Net_AddUser_Handler()),
- Map.entry("GetUser", new Net_GetUser_Handler()),
- Map.entry("SearchUsers", new Net_SearchUsers_Handler()),
-
- // --- auth ---
- Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
- Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()),
- Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
- Map.entry("ListSessions", new Net_ListSessions_Handler()),
-
- // --- login to existing session in 2 steps ---
- Map.entry("SessionChallenge", new Net_SessionChallenge_Handler()),
- Map.entry("SessionLogin", new Net_SessionLogin_Handler()),
-
- // --- blockchain ---
- Map.entry("AddBlock", new Net_AddBlock_Handler()),
-
- // --- userParams ---
- Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()),
- Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
- Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
-
- // --- connections ---
- Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler())
-
- // --- subscriptions ---
-// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
- );
-
- private static final Map> REQUEST_TYPES = Map.ofEntries(
- Map.entry("AddUser", Net_AddUser_Request.class),
- Map.entry("GetUser", Net_GetUser_Request.class),
- Map.entry("SearchUsers", Net_SearchUsers_Request.class),
-
- // --- auth ---
- Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),
- Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class),
- Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
- Map.entry("ListSessions", Net_ListSessions_Request.class),
-
- // --- NEW v2 ---
- Map.entry("SessionChallenge", Net_SessionChallenge_Request.class),
- Map.entry("SessionLogin", Net_SessionLogin_Request.class),
-
- // --- blockchain ---
- Map.entry("AddBlock", Net_AddBlock_Request.class),
-
- // --- userParams ---
- Map.entry("UpsertUserParam", Net_UpsertUserParam_Request.class),
- Map.entry("GetUserParam", Net_GetUserParam_Request.class),
- Map.entry("ListUserParams", Net_ListUserParams_Request.class),
-
-
- // --- connections ---
- Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class)
- );
-
- private JsonHandlerRegistry() { }
-
- public static Map getHandlers() {
- return HANDLERS;
- }
-
- public static Map> getRequestTypes() {
- return REQUEST_TYPES;
- }
-}
-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.Net_Exception_Response;
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-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-сообщений.
- *
- * 1) Парсит общий пакет (op, requestId, payload).
- * 2) По op выбирает класс запроса и хэндлер.
- * 3) Собирает "плоский" объект: op + requestId + поля из payload.
- * 4) Маппит его в NetRequest через ObjectMapper.
- * 5) Вызывает хэндлер, получает NetResponse.
- * 6) Собирает 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()
- .setSerializationInclusion(JsonInclude.Include.NON_NULL);
-
- private static final Map JSON_HANDLERS =
- JsonHandlerRegistry.getHandlers();
-
- private static final Map> JSON_REQUEST_TYPES =
- JsonHandlerRegistry.getRequestTypes();
-
- private JsonInboundProcessor() {
- // utility
- }
-
- public static String processJson(String json, ConnectionContext ctx) {
- String op = null;
- String requestId = null;
-
- // Для лога полезно знать, кто прислал (хотя бы login/sessionId, если есть)
- String ctxLogin = safe(ctx != null ? ctx.getLogin() : null);
- String ctxSessionId = safe(ctx != null ? ctx.getSessionId() : null);
-
- try {
- if (json == null || json.isBlank()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- null,
- null,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_JSON",
- "Пустое JSON-сообщение"
- );
-
- String out = writeResponse(err);
-
- // DEBUG: что пришло / что ушло
- if (log.isDebugEnabled()) {
- log.debug("JSON IN (login={}, sessionId={}): ", ctxLogin, ctxSessionId);
- log.debug("JSON OUT (login={}, sessionId={}): {}", ctxLogin, ctxSessionId, shorten(out, 1200));
- }
- return out;
- }
-
- // DEBUG: сырой вход (обрезаем, чтобы не убить лог)
- if (log.isDebugEnabled()) {
- log.debug("JSON IN (login={}, sessionId={}): {}", ctxLogin, ctxSessionId, shorten(json, 1200));
- }
-
- // 1) Парсим общий пакет
- JsonNode root = JSON_MAPPER.readTree(json);
-
- // 2) op и requestId из корня
- op = getTextOrNull(root, "op");
- requestId = getTextOrNull(root, "requestId");
-
- if (op == null || op.isEmpty()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- null,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "NO_OP",
- "Поле 'op' отсутствует или пустое"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- JsonMessageHandler handler = JSON_HANDLERS.get(op);
- Class extends Net_Request> reqClass = JSON_REQUEST_TYPES.get(op);
-
- if (handler == null || reqClass == null) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "UNKNOWN_OP",
- "Неизвестная операция: " + op
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // 3) Берём payload
- JsonNode payloadNode = root.get("payload");
- if (payloadNode == null || payloadNode.isNull()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "NO_PAYLOAD",
- "Поле 'payload' отсутствует"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
- if (!payloadNode.isObject()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "BAD_PAYLOAD",
- "Поле 'payload' должно быть объектом"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // 3.1 Собираем "плоский" объект для маппинга в NetRequest:
- // op + requestId + поля из payload
- ObjectNode merged = JSON_MAPPER.createObjectNode();
-
- // Добавляем op и requestId, чтобы они попали в NetRequest
- merged.put("op", op);
- if (requestId != null) merged.put("requestId", requestId);
-
- // Добавляем все поля из payload внутрь
- merged.setAll((ObjectNode) payloadNode);
-
- // 4) Маппим в конкретный класс NetRequest
- Net_Request request;
- try {
- request = JSON_MAPPER.treeToValue(merged, reqClass);
- } catch (Exception mapErr) {
- // Важно: вот это часто “теряется”, если не логировать отдельно
- log.error("❌ JSON map error (op={}, requestId={}, login={}, sessionId={}): merged={}",
- op, safe(requestId), ctxLogin, ctxSessionId, shorten(merged.toString(), 1200), mapErr);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "BAD_REQUEST_FORMAT",
- "Некорректный формат запроса: не удалось распарсить поля payload"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // DEBUG: нормализованный запрос (уже распарсен)
- if (log.isDebugEnabled()) {
- log.debug("REQ OBJ (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(safeToString(request), 1200));
- }
-
- // 5) Вызываем хэндлер
- Net_Response response;
- try {
- response = handler.handle(request, ctx);
- } catch (Exception handlerError) {
- // ✅ Вот тут как раз и должны “появляться ошибки в логере”
- log.error("💥 Handler error (op={}, requestId={}, login={}, sessionId={})",
- op, safe(requestId), ctxLogin, ctxSessionId, handlerError);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_HANDLER_ERROR",
- "Неожиданная ошибка при обработке операции: " + op
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // На всякий случай: если хэндлер не выставил op/requestId
- if (response.getOp() == null) response.setOp(op);
- if (response.getRequestId() == null) response.setRequestId(requestId);
-
- // 6) Универсальная сборка ответа
- String out = writeResponse(response);
-
- // DEBUG: ответ ушёл
- if (log.isDebugEnabled()) {
- log.debug("RESP OBJ (login={}, sessionId={}, op={}, requestId={}, status={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), response.getStatus(), shorten(safeToString(response), 1200));
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}, status={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), response.getStatus(), shorten(out, 1200));
- }
-
- return out;
-
- } catch (Exception e) {
- // ✅ Любая неожиданная ошибка парсинга/обработки — в лог
- log.error("❌ JSON processing error (op={}, requestId={}, login={}, sessionId={})",
- safe(op), safe(requestId), safe(ctxLogin), safe(ctxSessionId), e);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op != null ? op : "Unknown",
- requestId,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
-
- String out = writeResponse(err);
-
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
-
- return out;
- }
- }
-
- // --- helpers ---
-
- private static String getTextOrNull(JsonNode node, String field) {
- if (node == null || !node.has(field) || node.get(field).isNull()) return null;
- return node.get(field).asText();
- }
-
- /**
- * Унифицированная сериализация любого NetResponse в формат:
- * {
- * "op": ...,
- * "requestId": ...,
- * "status": ...,
- * "payload": { ... }
- * }
- */
- private static String writeResponse(Net_Response response) {
- try {
- // Конвертируем полный объект ответа в 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);
-
- // payload — это всё, что осталось от full (может быть пустым объектом {})
- root.set("payload", full);
-
- return JSON_MAPPER.writeValueAsString(root);
-
- } catch (Exception e) {
- // Совсем аварийный случай — сериализация ответа сломалась.
- log.error("❌ Response serialization error (op={}, requestId={})",
- safe(response != null ? response.getOp() : null),
- safe(response != null ? response.getRequestId() : null),
- e);
-
- return "{\"op\":\"" + safe(response != null ? response.getOp() : null) +
- "\",\"requestId\":\"" + safe(response != null ? response.getRequestId() : null) +
- "\",\"status\":" + (response != null ? response.getStatus() : 500) +
- ",\"payload\":{\"code\":\"SERIALIZATION_ERROR\",\"message\":\"Ошибка сериализации ответа\"}}";
- }
- }
-
- private static String safe(String s) {
- return s != null ? s : "";
- }
-
- private static String shorten(String s, int max) {
- if (s == null) return "";
- if (s.length() <= max) return s;
- return s.substring(0, Math.max(0, max)) + "...(+" + (s.length() - max) + " chars)";
- }
-
- private static String safeToString(Object o) {
- if (o == null) return "null";
- try {
- // Чтобы не плодить огромные логи и не утыкаться в циклические ссылки —
- // логируем как JSON, если возможно.
- return JSON_MAPPER.writeValueAsString(o);
- } catch (Exception ignore) {
- return String.valueOf(o);
- }
- }
-}
-package server.logic.ws_protocol.JSON.utils;
-
-import shine.db.entities.SolanaUserEntry;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-
-public final class AuthSignatures {
-
- private AuthSignatures() {}
-
- /** preimage для CreateAuthSession(v2): "AUTH_CREATE_SESSION:login:timeMs:authNonce" */
- public static byte[] preimageCreateAuthSession(String login, long timeMs, String authNonce) {
- String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
- return preimageStr.getBytes(StandardCharsets.UTF_8);
- }
-
- /** Декод base64 / base64url (если надо — подстрой под твой decodeBase64Any) */
- public static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
- if (s == null) throw new IllegalArgumentException("base64 is null");
- String x = s.trim();
- if (x.isEmpty()) throw new IllegalArgumentException("base64 is empty");
-
- try {
- return Base64.getDecoder().decode(x);
- } catch (IllegalArgumentException e1) {
- // пробуем base64url без паддинга
- return Base64.getUrlDecoder().decode(x);
- }
- }
-
- /**
- * Проверка подписи CreateAuthSession(v2) по deviceKey пользователя.
- * Подпись проверяется над preimageCreateAuthSession(...).
- */
- public static boolean verifyCreateAuthSessionSignature(
- SolanaUserEntry user,
- String login,
- String authNonce,
- long timeMs,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // user.getDeviceKey() — base64 публичного ключа (32 байта)
- byte[] publicKey32 = decodeBase64Any(user.getDeviceKey());
- byte[] signature64 = decodeBase64Any(signatureB64);
-
- byte[] preimage = preimageCreateAuthSession(login, timeMs, authNonce);
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-}
-package server.logic.ws_protocol.JSON.utils;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Фабрика ошибок для JSON-протокола.
- * Создаёт единообразные NetExceptionResponse.
- */
-public final class NetExceptionResponseFactory {
-
- private NetExceptionResponseFactory() {
- // запрет на создание объектов
- }
-
- public static Net_Exception_Response error(Net_Request req,
- int status,
- String code,
- String message) {
-
- Net_Exception_Response resp = new Net_Exception_Response();
-
- // ✅ НЕ падаем, даже если req == null
- if (req != null) {
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- } else {
- resp.setOp(null);
- resp.setRequestId(null);
- }
-
- resp.setStatus(status);
- resp.setCode(code);
- resp.setMessage(message);
- return resp;
- }
-
- /**
- * Вариант для случаев, когда NetRequest ещё не распарсен,
- * но мы уже знаем op и requestId (или они null).
- */
- public static Net_Exception_Response error(String op,
- String requestId,
- int status,
- String code,
- String message) {
-
- Net_Exception_Response resp = new Net_Exception_Response();
- resp.setOp(op);
- resp.setRequestId(requestId);
- resp.setStatus(status);
- resp.setCode(code);
- resp.setMessage(message);
- return resp;
- }
-}
-package server.logic.ws_protocol;
-
-/**
- * WireCodes — константы бинарного протокола поверх WebSocket.
- *.
- * Формат входящего сообщения:
- * [4] int opCode (big-endian)
- * [*] payload
- *.
- * Ответ сервера:
- * ровно [4] int statusCode (big-endian)
- */
-public final class WireCodes {
- private WireCodes() {}
-
- public static final class Op {
- public static final int PING = 0;
- public static final int ADD_BLOCK = 1;
- public static final int GET_BLOCKCHAIN = 2;
- public static final int SEARCH_USERS = 30;
- public static final int GET_LAST_BLOCK_INFO = 31;
- private Op() {}
- }
-
- public static final class Status {
- public static final int PONG = 100; // ответ на PING
-// public static final int OK = 200; // успех
-
- public static final int ALREADY_EXISTS = 409; // пришёл блок < N+1
- public static final int NON_SEQUENTIAL = 412; // пришёл блок > N+1
- public static final int NOT_FOUND = 422; // Нет такого полбзователя - типо добавляем блок к которому нет пользователя - хотя на деле такой статус наверное никогда не вернётся, тк это раньше проверяется
-
-
- private Status() {}
-
-
-
-
- // ============================================================
- // 🟢 УСПЕШНЫЕ ОПЕРАЦИИ
- // ============================================================
-
- /** ✅ Блок успешно добавлен в цепочку. */
- public static final int OK = 200;
-
- /** 🌱 Создана новая цепочка (первый блок-заголовок принят). */
- public static final int CHAIN_CREATED = 201;
-
- /**
- * 🔁 Такой блок уже существует.
- * Клиент может считать это успешным ответом:
- * - сервер возвращает 8 байт: [4] код (202) + [4] номер последнего блока (int)
- * - клиент обновляет свой lastBlockNumber и не пересылает этот блок снова. */
- public static final int BLOCK_ALREADY_EXISTS = 202; // плюс к кодуследом возвращается номер последнего блока на сервере
-
-
- // ============================================================
- // 🟡 ЛОГИЧЕСКИЕ / ПРОТОКОЛЬНЫЕ ОШИБКИ
- // ============================================================
-
- /** ⚠️ Нарушена последовательность — пришёл блок с номером > ожидаемого.
- * Сервер вернёт 8 байт: [4] код (409) + [4] последний номер блока.
- * Клиент должен дослать недостающие блоки. */
- public static final int OUT_OF_SEQUENCE = 409; // плюс к кодуследом возвращается номер последнего блока на сервере
-
- /** ❌ Некорректные или неполные данные в запросе. */
- public static final int BAD_REQUEST = 400;
-
- /** 🚫 Цепочка с указанным blockchainId не найдена. */
- public static final int CHAIN_NOT_FOUND = 404;
-
- /** 🧩 Несовпадение blockchainId между заголовком блока и телом. */
- public static final int INVALID_BLOCKCHAIN_ID = 421;
-
- /** ❌ Ошибка верификации блока — хэш или подпись не совпали.
- * 🔐 Ошибка хэша: SHA-256(preimage) не совпал с переданным hash32.
- * 🔏 Ошибка подписи Ed25519 — блок не прошёл криптографическую проверку. */
- public static final int UNVERIFIED = 422;
-
-
- /** 🙅 Некорректный логин (пустой, неверный формат, недопустимые символы). По сути вообще не может быть, тк логин проверяют при создании в другом блокчейне*/
- public static final int BAD_LOGIN = 462;
-
-
- // ============================================================
- // 🔴 СИСТЕМНЫЕ ОШИБКИ / ОГРАНИЧЕНИЯ
- // ============================================================
-
- // ============================================================
- // 🔴 СИСТЕМНЫЕ ОШИБКИ / ОГРАНИЧЕНИЯ
- // ============================================================
-
- /** 💾 Достигнут лимит размера блокчейна. */
- public static final int BLOCKCHAIN_FULL = 507;
-
- /** 🧱 Ошибка при сохранении или обновлении данных на сервере (файлы, JSON и т.п.). */
- public static final int SERVER_DATA_ERROR = 501;
-
- /** 💥 Общая внутренняя ошибка сервера (необработанное исключение). */
- public static final int INTERNAL_ERROR = 500;
- }
-
-}
-
-package server.ws;
-
-import org.eclipse.jetty.websocket.api.Session;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-import server.logic.ws_protocol.JSON.ConnectionContext;
-import shine.db.entities.SolanaUserEntry;
-
-import java.net.SocketAddress;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * Утилита для работы с WebSocket-подключениями.
- *
- * Цель этой версии:
- * - всегда логировать "кто закрыл" / "что закрывали" / "в каком состоянии был WS";
- * - логировать исключения так, чтобы было видно первопричину;
- * - не терять контекст из-за ctx.reset() (сначала снимаем "снимок" полей).
- */
-public final class WsConnectionUtils {
-
- private static final Logger log = LoggerFactory.getLogger(WsConnectionUtils.class);
-
- /** Счётчик событий закрытия (удобно коррелировать логи). */
- private static final AtomicLong CLOSE_SEQ = new AtomicLong(0);
-
- private WsConnectionUtils() {
- // utility
- }
-
- public static void closeConnection(ConnectionContext ctx, int statusCode, String reason) {
- closeConnection(ctx, statusCode, reason, null, "UNKNOWN");
- }
-
- /**
- * Расширенное закрытие с указанием инициатора и причины (Throwable).
- *
- * @param ctx контекст
- * @param statusCode код закрытия
- * @param reason причина (пойдёт в close frame + логи)
- * @param cause исключение/первопричина (если закрываем из catch)
- * @param initiator строка "кто инициировал" (handler/op/requestId/etc.)
- */
- public static void closeConnection(ConnectionContext ctx,
- int statusCode,
- String reason,
- Throwable cause,
- String initiator) {
- if (ctx == null) return;
-
- final long closeId = CLOSE_SEQ.incrementAndGet();
-
- // --- СНИМОК КОНТЕКСТА ДО reset() ---
- final Session ws = ctx.getWsSession();
-
- final String sessionId = safeString(ctx.getSessionId());
- final int authStatus = safeAuthStatus(ctx);
-
- final SolanaUserEntry user = ctx.getSolanaUser();
- final String login = (user != null ? safeString(user.getLogin()) : "");
-
- final String activeSessionId =
- (ctx.getActiveSession() != null ? safeString(ctx.getActiveSession().getSessionId()) : "");
-
- final boolean wsPresent = (ws != null);
- final boolean wsOpen = (ws != null && safeIsOpen(ws));
- final String wsInfo = formatWsInfo(ws);
-
- final String threadName = Thread.currentThread().getName();
- final int ctxId = System.identityHashCode(ctx);
-
- // Логируем "начало закрытия" всегда, чтобы видеть даже случаи "ws уже закрыт"
- if (cause != null) {
- log.warn("WS_CLOSE#{} BEGIN initiator={} thread={} ctxId={} login={} sessionId={} activeSessionId={} authStatus={} statusCode={} reason={} wsPresent={} wsOpen={} wsInfo={}",
- closeId, initiator, threadName, ctxId, login, sessionId, activeSessionId, authStatus, statusCode, reason, wsPresent, wsOpen, wsInfo, cause);
- } else {
- log.info("WS_CLOSE#{} BEGIN initiator={} thread={} ctxId={} login={} sessionId={} activeSessionId={} authStatus={} statusCode={} reason={} wsPresent={} wsOpen={} wsInfo={}",
- closeId, initiator, threadName, ctxId, login, sessionId, activeSessionId, authStatus, statusCode, reason, wsPresent, wsOpen, wsInfo);
- }
-
- // --- ШАГ 1: убрать из реестра (чтобы новые сообщения не шли в мёртвый контекст) ---
- try {
- ActiveConnectionsRegistry.getInstance().remove(ctx);
- log.debug("WS_CLOSE#{} registry.remove OK ctxId={} sessionId={} login={}", closeId, ctxId, sessionId, login);
- } catch (Exception e) {
- log.warn("WS_CLOSE#{} registry.remove FAIL ctxId={} sessionId={} login={}", closeId, ctxId, sessionId, login, e);
- }
-
- // --- ШАГ 2: закрыть WS (если открыт) ---
- if (ws != null) {
- if (safeIsOpen(ws)) {
- try {
- ws.close(statusCode, safeString(reason));
- log.info("WS_CLOSE#{} ws.close OK ctxId={} sessionId={} login={} statusCode={} reason={}",
- closeId, ctxId, sessionId, login, statusCode, reason);
- } catch (Exception e) {
- log.warn("WS_CLOSE#{} ws.close FAIL ctxId={} sessionId={} login={} statusCode={} reason={} wsInfo={}",
- closeId, ctxId, sessionId, login, statusCode, reason, wsInfo, e);
- }
- } else {
- log.info("WS_CLOSE#{} ws already closed ctxId={} sessionId={} login={} wsInfo={}",
- closeId, ctxId, sessionId, login, wsInfo);
- }
- }
-
- // --- ШАГ 3: очистить контекст (в конце, чтобы не потерять поля в логах выше) ---
- try {
- ctx.reset();
- log.debug("WS_CLOSE#{} ctx.reset OK ctxId={} (was sessionId={}, login={})", closeId, ctxId, sessionId, login);
- } catch (Exception e) {
- log.warn("WS_CLOSE#{} ctx.reset FAIL ctxId={} (was sessionId={}, login={})", closeId, ctxId, sessionId, login, e);
- }
-
- log.info("WS_CLOSE#{} END initiator={} ctxId={} sessionId={} login={}", closeId, initiator, ctxId, sessionId, login);
- }
-
- private static String safeString(String s) {
- return (s == null ? "" : s);
- }
-
- private static int safeAuthStatus(ConnectionContext ctx) {
- try {
- return ctx.getAuthenticationStatus();
- } catch (Exception e) {
- return -999;
- }
- }
-
- private static boolean safeIsOpen(Session ws) {
- try {
- return ws.isOpen();
- } catch (Exception e) {
- return false;
- }
- }
-
- private static String formatWsInfo(Session ws) {
- if (ws == null) return "null";
-
- String remote = "";
- String local = "";
- try {
- SocketAddress ra = ws.getRemoteAddress();
- remote = (ra != null ? ra.toString() : "");
- } catch (Exception ignored) { }
-
- try {
- SocketAddress la = ws.getLocalAddress();
- local = (la != null ? la.toString() : "");
- } catch (Exception ignored) { }
-
- return "remote=" + remote + ", local=" + local;
- }
-}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java
index 518e9c8..742c851 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java
@@ -10,7 +10,7 @@ import shine.db.entities.ActiveSessionEntry;
*
* Важно (v2):
* - Авторизация всегда 2 шага:
- * A) Создание новой сессии через deviceKey:
+ * A) Создание новой сессии через clientKey:
* AuthChallenge(login) -> ctx.authNonce
* CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
*
@@ -39,7 +39,7 @@ public class ConnectionContext {
/**
* Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
- * используется на шаге CreateAuthSession для проверки подписи deviceKey.
+ * используется на шаге CreateAuthSession для проверки подписи clientKey.
*/
private String authNonce;
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/all_files.txt b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/all_files.txt
deleted file mode 100644
index 59f8d1b..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/all_files.txt
+++ /dev/null
@@ -1,4548 +0,0 @@
-package server.logic.ws_protocol.JSON;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-/**
- * Реестр активных подключений (только авторизованные).
- */
-public final class ActiveConnectionsRegistry {
-
- private static final Logger log = LoggerFactory.getLogger(ActiveConnectionsRegistry.class);
-
- private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry();
-
- public static ActiveConnectionsRegistry getInstance() {
- return INSTANCE;
- }
-
- private ActiveConnectionsRegistry() {
- // singleton
- }
-
- // sessionId (String) -> ConnectionContext
- private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>();
-
- // login (String) -> множество ConnectionContext для этого пользователя
- private final ConcurrentHashMap> byLogin = new ConcurrentHashMap<>();
-
- /**
- * Зарегистрировать авторизованное подключение.
- * Ожидается, что в ctx уже выставлены login и sessionId.
- */
- public void register(ConnectionContext ctx) {
- if (ctx == null) return;
-
- String sessionId = ctx.getSessionId();
- String login = ctx.getLogin();
-
- if (sessionId == null || sessionId.isBlank() || login == null || login.isBlank()) {
- log.debug("register skipped: bad ctx fields (login='{}', sessionId='{}')", login, sessionId);
- return;
- }
-
- // ✅ Если кто-то перерегистрировал тот же sessionId — вычищаем старый ctx из byLogin
- ConnectionContext prev = bySessionId.put(sessionId, ctx);
- if (prev != null && prev != ctx) {
- String prevLogin = prev.getLogin();
- if (prevLogin != null && !prevLogin.isBlank()) {
- Set prevSet = byLogin.get(prevLogin);
- if (prevSet != null) {
- prevSet.remove(prev);
- if (prevSet.isEmpty()) {
- byLogin.remove(prevLogin);
- }
- }
- }
- log.warn("sessionId reused: replaced previous ctx (sessionId={}, prevLogin={}, newLogin={})",
- sessionId, prevLogin, login);
- }
-
- byLogin
- .computeIfAbsent(login, id -> new CopyOnWriteArraySet<>())
- .add(ctx);
-
- log.debug("registered ctx (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Удалить подключение по контексту (например, при onClose).
- */
- public void remove(ConnectionContext ctx) {
- if (ctx == null) return;
-
- String sessionId = ctx.getSessionId();
- String login = ctx.getLogin();
-
- if (sessionId != null && !sessionId.isBlank()) {
- ConnectionContext removed = bySessionId.remove(sessionId);
-
- // Если в мапе лежал другой ctx под тем же sessionId — не трогаем его byLogin
- if (removed != null && removed != ctx) {
- log.debug("remove(ctx): sessionId mapped to another ctx, skip byLogin cleanup (sessionId={})", sessionId);
- return;
- }
- }
-
- if (login != null && !login.isBlank()) {
- Set set = byLogin.get(login);
- if (set != null) {
- set.remove(ctx);
- if (set.isEmpty()) {
- byLogin.remove(login);
- }
- }
- }
-
- log.debug("removed ctx (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Удалить подключение по sessionId.
- */
- public void removeBySessionId(String sessionId) {
- if (sessionId == null || sessionId.isBlank()) return;
-
- ConnectionContext ctx = bySessionId.remove(sessionId);
- if (ctx == null) return;
-
- String login = ctx.getLogin();
- if (login != null && !login.isBlank()) {
- Set set = byLogin.get(login);
- if (set != null) {
- set.remove(ctx);
- if (set.isEmpty()) {
- byLogin.remove(login);
- }
- }
- }
-
- log.debug("removed by sessionId (login={}, sessionId={})", login, sessionId);
- }
-
- /**
- * Получить контекст по sessionId.
- */
- public ConnectionContext getBySessionId(String sessionId) {
- if (sessionId == null || sessionId.isBlank()) return null;
- return bySessionId.get(sessionId);
- }
-
- /**
- * Получить все активные подключения пользователя по login.
- */
- public Set getByLogin(String login) {
- if (login == null || login.isBlank()) return Set.of();
- Set set = byLogin.get(login);
- return (set == null) ? Set.of() : set; // CopyOnWriteArraySet можно отдавать как есть
- }
-}
-package server.logic.ws_protocol.JSON;
-
-import org.eclipse.jetty.websocket.api.Session;
-import shine.db.entities.SolanaUserEntry;
-import shine.db.entities.ActiveSessionEntry;
-
-/**
- * ConnectionContext — контекст состояния одного WebSocket-соединения.
- * Живёт ровно столько же, сколько живёт подключение.
- *
- * Важно (v2):
- * - Авторизация всегда 2 шага:
- * A) Создание новой сессии через deviceKey:
- * AuthChallenge(login) -> ctx.authNonce
- * CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
- *
- * B) Вход в существующую сессию через sessionKey:
- * SessionChallenge(sessionId) -> ctx.sessionLoginNonce + ctx.sessionLoginSessionId + expiresAt
- * SessionLogin(...) -> проверка подписи sessionKey по pubkey из БД -> ctx.AUTH_STATUS_USER
- */
-public class ConnectionContext {
-
- // Статусы аутентификации
- public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
- public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
- public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
-
- // Полный пользователь из БД (solana_users)
- private SolanaUserEntry solanaUserEntry;
-
- // Активная сессия из БД (active_sessions)
- private ActiveSessionEntry activeSessionEntry;
-
- /**
- * Идентификатор сессии — base64-строка от 32 байт.
- * Заполняется после успешного входа (AUTH_STATUS_USER).
- */
- private String sessionId;
-
- /**
- * Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
- * используется на шаге CreateAuthSession для проверки подписи deviceKey.
- */
- private String authNonce;
-
- /* ===================== SessionLogin challenge (v2) ===================== */
-
- /**
- * Одноразовый nonce, выданный на шаге SessionChallenge(sessionId),
- * используется на шаге SessionLogin для проверки подписи sessionKey.
- */
- private String sessionLoginNonce;
-
- /**
- * sessionId, для которого был выдан sessionLoginNonce.
- * Нужен, чтобы SessionLogin не мог "подставить" другой sessionId.
- */
- private String sessionLoginSessionId;
-
- /**
- * Время истечения sessionLoginNonce (мс с 1970-01-01).
- * Если текущее время > expiresAt, то nonce считается недействительным.
- */
- private long sessionLoginNonceExpiresAtMs;
-
- /* ====================================================================== */
-
- /**
- * Текущий статус аутентификации.
- * См. константы AUTH_STATUS_*
- */
- private int authenticationStatus = AUTH_STATUS_NONE;
-
- /**
- * WebSocket-сессия Jetty для данного подключения.
- * Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту.
- */
- private Session wsSession;
-
- // --- WebSocket Session ---
-
- public Session getWsSession() {
- return wsSession;
- }
-
- public void setWsSession(Session wsSession) {
- this.wsSession = wsSession;
- }
-
- // --- SolanaUser / ActiveSession ---
-
- public SolanaUserEntry getSolanaUser() {
- return solanaUserEntry;
- }
-
- public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
- this.solanaUserEntry = solanaUserEntry;
- }
-
- public ActiveSessionEntry getActiveSession() {
- return activeSessionEntry;
- }
-
- public void setActiveSession(ActiveSessionEntry activeSessionEntry) {
- this.activeSessionEntry = activeSessionEntry;
- }
-
- // --- Удобный геттер для логина ---
-
- public String getLogin() {
- return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
- }
-
- // --- sessionId ---
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- // --- authNonce ---
-
- public String getAuthNonce() {
- return authNonce;
- }
-
- public void setAuthNonce(String authNonce) {
- this.authNonce = authNonce;
- }
-
- // --- sessionLoginNonce (v2) ---
-
- public String getSessionLoginNonce() {
- return sessionLoginNonce;
- }
-
- public void setSessionLoginNonce(String sessionLoginNonce) {
- this.sessionLoginNonce = sessionLoginNonce;
- }
-
- public String getSessionLoginSessionId() {
- return sessionLoginSessionId;
- }
-
- public void setSessionLoginSessionId(String sessionLoginSessionId) {
- this.sessionLoginSessionId = sessionLoginSessionId;
- }
-
- public long getSessionLoginNonceExpiresAtMs() {
- return sessionLoginNonceExpiresAtMs;
- }
-
- public void setSessionLoginNonceExpiresAtMs(long sessionLoginNonceExpiresAtMs) {
- this.sessionLoginNonceExpiresAtMs = sessionLoginNonceExpiresAtMs;
- }
-
- // --- auth status ---
-
- public int getAuthenticationStatus() {
- return authenticationStatus;
- }
-
- public void setAuthenticationStatus(int authenticationStatus) {
- this.authenticationStatus = authenticationStatus;
- }
-
- public boolean isAuthenticatedUser() {
- return authenticationStatus == AUTH_STATUS_USER;
- }
-
- public boolean isAnonymous() {
- return authenticationStatus == AUTH_STATUS_NONE;
- }
-
- public void reset() {
- solanaUserEntry = null;
- activeSessionEntry = null;
-
- sessionId = null;
- authNonce = null;
-
- sessionLoginNonce = null;
- sessionLoginSessionId = null;
- sessionLoginNonceExpiresAtMs = 0;
-
- authenticationStatus = AUTH_STATUS_NONE;
- wsSession = null;
- }
-
- @Override
- public String toString() {
- return "ConnectionContext{" +
- "login='" + getLogin() + '\'' +
- ", sessionId=" + sessionId +
- ", authenticationStatus=" + authenticationStatus +
- '}';
- }
-}
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех событий (event).
- * Общие поля: op и payload.
- *.
- * Формат JSON (event):
- * {
- * "op": "...",
- * "payload": { ... }
- * }
- */
-public abstract class Net_Event {
-
- /** Имя операции / события (op). */
- private String op;
-
- /**
- * Произвольные данные.
- * В JSON это поле "payload".
- */
- private Object payload;
-
- // --- getters / setters ---
-
- public String getOp() {
- return op;
- }
-
- public void setOp(String op) {
- this.op = op;
- }
-
- public Object getPayload() {
- return payload;
- }
-
- public void setPayload(Object payload) {
- this.payload = payload;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Ответ с ошибкой (любой отказ).
- *.
- * В payload будет:
- * {
- * "code": "...",
- * "message": "..."
- * }
- */
-public class Net_Exception_Response extends Net_Response {
-
- 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;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех запросов (client → server).
- *.
- * Наследуется от NetEvent и добавляет requestId.
- *.
- * Формат JSON (request):
- * {
- * "op": "...",
- * "requestId": "...",
- * "payload": { ... }
- * }
- */
-public abstract class Net_Request extends Net_Event {
-
- /** Идентификатор запроса, чтобы связать запрос и ответ. */
- private String requestId;
-
- // --- getters / setters ---
-
- public String getRequestId() {
- return requestId;
- }
-
- public void setRequestId(String requestId) {
- this.requestId = requestId;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех ответов (server → client).
- *.
- * Наследуется от NetRequest и добавляет status.
- *.
- * Формат JSON (response):
- * {
- * "op": "...",
- * "requestId": "...",
- * "status": 200,
- * "payload": { ... } // и для успеха, и для ошибки
- * }
- */
-public abstract class Net_Response extends Net_Request {
-
- /** Статус результата (200 — успех, любое другое значение — ошибка). */
- private int status;
-
- // --- getters / setters ---
-
- public int getStatus() {
- return status;
- }
-
- public void setStatus(int status) {
- this.status = status;
- }
-
- public boolean isOk() {
- return status == 200;
- }
-}
-
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 авторизации: запрос выдачи одноразового nonce (authNonce).
- *
- * Клиент по логину просит сервер сгенерировать случайный authNonce,
- * который будет использован на втором шаге при подписи.
- *
- * Формат входящего JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "payload": {
- * "login": "someLogin"
- * }
- * }
- *
- * Формат успешного ответа:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Request extends Net_Request {
-
- /**
- * Логин пользователя, для которого запускается авторизация.
- */
- private String login;
-
- public String getLogin() {
- return login;
- }
- public void setLogin(String login) {
- this.login = login;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на AuthChallenge.
- *
- * При успехе сервер возвращает одноразовый nonce для подписи (authNonce),
- * который клиент обязан использовать на втором шаге при формировании строки
- * для цифровой подписи.
- *
- * JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Response extends Net_Response {
-
- /**
- * Одноразовый nonce для авторификации.
- * Строка — это base64-представление 32 случайных байт.
- */
- private String authNonce;
-
- public String getAuthNonce() {
- return authNonce;
- }
-
- public void setAuthNonce(String authNonce) {
- this.authNonce = authNonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос CloseActiveSession — закрытие активной сессии пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и "AUTH_IN_PROGRESS" здесь больше нет.
- *
- * payload:
- * {
- * "sessionId": "..." // опционально; если пусто — закрываем текущую
- * }
- */
-public class Net_CloseActiveSession_Request extends Net_Request {
-
- /** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CloseActiveSession.
- *
- * При успехе:
- * - status = 200;
- * - payload = {}.
- *
- * Закрытие WebSocket-соединения может быть выполнено сразу (для другой сессии)
- * или чуть позже (для текущей сессии) после отправки ответа.
- */
-public class Net_CloseActiveSession_Response extends Net_Response {
- // Дополнительных полей пока не требуется.
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
- *
- * Шаги:
- * 1) AuthChallenge(login) -> authNonce
- * 2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
- *
- * Подпись deviceKey делается над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
- *
- * Важно:
- * - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
- * - В БД active_sessions.session_key хранится sessionPubKeyB64.
- */
-public class Net_CreateAuthSession_Request extends Net_Request {
-
- /** Клиентский пароль для хранения данных (base64 от 32 байт). */
- private String storagePwd;
-
- /** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
- private String sessionPubKeyB64;
-
- /** Время на стороне клиента (мс с 1970-01-01). */
- private long timeMs;
-
- /** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-
- public String getSessionPubKeyB64() {
- return sessionPubKeyB64;
- }
-
- public void setSessionPubKeyB64(String sessionPubKeyB64) {
- this.sessionPubKeyB64 = sessionPubKeyB64;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CreateAuthSession (v2).
- *
- * При успехе сервер создаёт запись в active_sessions
- * и возвращает идентификатор сессии sessionId.
- *
- * JSON:
- * {
- * "op": "CreateAuthSession",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "sessionId": "base64(32)"
- * }
- * }
- */
-public class Net_CreateAuthSession_Response extends Net_Response {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListSessions — список активных сессий пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Пустой payload.
- */
-public class Net_ListSessions_Request extends Net_Request {
- // пусто
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.List;
-
-/**
- * Ответ на ListSessions.
- *
- * При успехе:
- * - status = 200;
- * - payload:
- * {
- * "sessions": [
- * {
- * "sessionId": "...",
- * "clientInfoFromClient": "...",
- * "clientInfoFromRequest": "...",
- * "geo": "Country, City" | "unknown",
- * "lastAuthirificatedAtMs": 1733310000000
- * },
- * ...
- * ]
- * }
- */
-public class Net_ListSessions_Response extends Net_Response {
-
- /**
- * Список активных сессий для текущего пользователя.
- */
- private List sessions;
-
- public List getSessions() {
- return sessions;
- }
-
- public void setSessions(List sessions) {
- this.sessions = sessions;
- }
-
- /**
- * Описание одной активной сессии.
- */
- public static class SessionInfo {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- /** Что прислал клиент в CreateAuthSession/RefreshSession (clientInfo). */
- private String clientInfoFromClient;
-
- /** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
- private String clientInfoFromRequest;
-
- /** Строка геолокации вида "Country, City" или "unknown". */
- private String geo;
-
- /** Время последней успешной авторизации/refresh (мс с 1970-01-01). */
- private long lastAuthirificatedAtMs;
-
- // --- getters / setters ---
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- public String getClientInfoFromClient() {
- return clientInfoFromClient;
- }
-
- public void setClientInfoFromClient(String clientInfoFromClient) {
- this.clientInfoFromClient = clientInfoFromClient;
- }
-
- public String getClientInfoFromRequest() {
- return clientInfoFromRequest;
- }
-
- public void setClientInfoFromRequest(String clientInfoFromRequest) {
- this.clientInfoFromRequest = clientInfoFromRequest;
- }
-
- public String getGeo() {
- return geo;
- }
-
- public void setGeo(String geo) {
- this.geo = geo;
- }
-
- public long getLastAuthirificatedAtMs() {
- return lastAuthirificatedAtMs;
- }
-
- public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
- this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 входа в существующую сессию (v2):
- * SessionChallenge(sessionId) -> nonce
- */
-public class Net_SessionChallenge_Request extends Net_Request {
-
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionChallenge (v2).
- * payload: { "nonce": "base64(32)" }
- */
-public class Net_SessionChallenge_Response extends Net_Response {
-
- private String nonce;
-
- public String getNonce() {
- return nonce;
- }
-
- public void setNonce(String nonce) {
- this.nonce = nonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 входа в существующую сессию (v2):
- * SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
- *
- * Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- *
- * nonce берётся из SessionChallenge и хранится в ctx (одноразовый, TTL).
- */
-public class Net_SessionLogin_Request extends Net_Request {
-
- private String sessionId;
- private long timeMs;
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionLogin (v2).
- * payload: { "storagePwd": "base64(32)" }
- */
-public class Net_SessionLogin_Response extends Net_Response {
-
- private String storagePwd;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.security.SecureRandom;
-
-/**
- * AuthChallenge (v2) — шаг 1 создания новой сессии.
- *
- * Логика авторизации (v2):
- * - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
- * - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
- * CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
- *
- * Что делает:
- * 1) Проверяет login.
- * 2) Находит пользователя (solana_users).
- * 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
- * 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
- */
-public class Net_AuthChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_AuthChallenge_Request req = (Net_AuthChallenge_Request) baseReq;
-
- String login = req.getLogin();
- if (login == null || login.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_LOGIN",
- "Пустой логин"
- );
- }
-
- // Если по этому соединению уже есть залогиненный пользователь — не даём повторную авторификацию
- if (ctx.getLogin() != null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "ALREADY_AUTHED",
- "Попытка повторной авторификации для уже заданного login=" + ctx.getLogin()
- );
- }
-
- SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
- if (solanaUserEntry == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "UNKNOWN_USER",
- "Пользователь с таким логином не найден"
- );
- }
-
- ctx.setSolanaUser(solanaUserEntry);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String authNonce = Base64Ws.encode(buf);
-
- ctx.setAuthNonce(authNonce);
-
- Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setAuthNonce(authNonce);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-/**
- * CloseActiveSession (v2) — закрытие текущей или другой сессии.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и AUTH_IN_PROGRESS здесь больше нет.
- *
- * Закрытие:
- * - удаляем запись из БД
- * - если по sessionId есть активный WS — закрываем его
- */
-public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- String targetSessionId = req.getSessionId();
- if (targetSessionId == null || targetSessionId.isBlank()) {
- if (ctx.getSessionId() != null && !ctx.getSessionId().isBlank()) {
- targetSessionId = ctx.getSessionId();
- } else if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
- targetSessionId = ctx.getActiveSession().getSessionId();
- } else {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_SESSION_TO_CLOSE",
- "Не удалось определить, какую сессию нужно закрыть"
- );
- }
- }
-
- ActiveSessionEntry targetSession;
- try {
- targetSession = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных при поиске сессии"
- );
- }
-
- if (targetSession == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия для закрытия не найдена"
- );
- }
-
- if (currentLogin == null || !currentLogin.equals(targetSession.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_OF_ANOTHER_USER",
- "Нельзя закрывать сессию другого пользователя"
- );
- }
-
- boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
-
- closeActiveSession(targetSessionId, ctx, isCurrentSession);
-
- Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- return resp;
- }
-
- private void closeActiveSession(String targetSessionId,
- ConnectionContext currentCtx,
- boolean isCurrentSession) {
-
- try {
- ActiveSessionsDAO.getInstance().deleteBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
- }
-
- ConnectionContext ctxToClose =
- ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
-
- if (ctxToClose == null) return;
-
- if (isCurrentSession && ctxToClose == currentCtx) {
- new Thread(() -> {
- try { Thread.sleep(50); } catch (InterruptedException ignored) {}
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }, "CloseSession-" + targetSessionId).start();
- } else {
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import org.eclipse.jetty.websocket.api.Session;
-
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.sql.SQLException;
-
-/**
- * CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
- *
- * Логика авторизации (v2):
- * - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
- * - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
- * отправляет на сервер ТОЛЬКО sessionPubKeyB64.
- * - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
- *
- * Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
- *
- * На выходе:
- * - создаётся запись active_sessions
- * - ctx становится AUTH_STATUS_USER (вход выполнен как "текущая сессия")
- * - ответ: sessionId
- */
-public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
- private static final SecureRandom RANDOM = new SecureRandom();
-
- public static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
-
- if (ctx == null
- || ctx.getSolanaUser() == null
- || ctx.getAuthNonce() == null
- || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
-
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_STEP1_CONTEXT",
- "Шаг 1 авторизации не был корректно выполнен для данного соединения"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no step1 context or bad auth state");
- return err;
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String login = user.getLogin();
- if (login == null || login.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_LOGIN",
- "Для пользователя не задан login в БД"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no login");
- return err;
- }
-
- String storagePwd = req.getStoragePwd();
- if (storagePwd == null || storagePwd.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_STORAGE_PWD",
- "Пустой storagePwd"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty storagePwd");
- return err;
- }
-
- String sessionPubKeyB64 = req.getSessionPubKeyB64();
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_PUBKEY",
- "Пустой sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
- return err;
- }
-
- // Проверим, что sessionPubKeyB64 декодируется в 32 байта
- byte[] sessionPubKey32;
- try {
- sessionPubKey32 = Base64Ws.decode(sessionPubKeyB64);
- } catch (IllegalArgumentException e) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный base64 в sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
- return err;
- }
- if (sessionPubKey32.length != 32) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_SESSION_PUBKEY_LEN",
- "sessionPubKey должен быть 32 байта"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
- return err;
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая цифровая подпись"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty signature");
- return err;
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- long diff = Math.abs(nowMs - timeMs);
- if (diff > ALLOWED_SKEW_MS) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: time skew");
- return err;
- }
-
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String devicePubKeyB64 = user.getDeviceKey();
- if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_DEVICE_KEY",
- "Отсутствует deviceKey у пользователя"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no deviceKey");
- return err;
- }
-
- String authNonce = ctx.getAuthNonce();
-
- boolean sigOk;
- try {
- sigOk = verifyCreateSessionSignature(
- user,
- login,
- authNonce,
- timeMs,
- signatureB64
- );
- } catch (IllegalArgumentException ex) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный формат Base64 для ключа или подписи"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad base64");
- return err;
- }
-
- if (!sigOk) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad signature");
- return err;
- }
-
- // --- генерируем sessionId ---
- String sessionId = generateRandom32B64Url();
- long now = System.currentTimeMillis();
-
- // --- Сбор данных о клиенте (IP, UA, язык) ---
- Session wsSession = ctx.getWsSession();
- String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
- String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
-
- String clientIp = "";
- if (wsSession != null) {
- String ip = ClientInfoService.extractClientIp(wsSession);
- if (ip != null) clientIp = ip;
-
- if (!clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- // --- создаём запись ActiveSession и сохраняем в БД ---
- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
- ActiveSessionEntry activeSessionEntry;
-
- try {
- activeSessionEntry = new ActiveSessionEntry(
- sessionId,
- login,
- sessionPubKeyB64, // session_key (pubkey)
- storagePwd,
- now,
- now,
- null, // pushEndpoint
- null, // pushP256dhKey
- null, // pushAuthKey
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
-
- dao.insert(activeSessionEntry);
- } catch (SQLException e) {
- log.error("Ошибка БД при создании новой сессии для login={}", login, e);
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_SESSION_CREATE",
- "Ошибка БД при создании сессии"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db error");
- return err;
- }
-
- // --- обновляем контекст ---
- ctx.setActiveSession(activeSessionEntry);
- ctx.setSessionId(sessionId);
- ctx.setAuthNonce(null);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // --- формируем ответ ---
- Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessionId(sessionId);
- return resp;
- }
-
- private static boolean verifyCreateSessionSignature(
- SolanaUserEntry user,
- String login,
- String authNonce,
- long timeMs,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // deviceKey (pub, 32)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
- byte[] signature64 = Base64Ws.decode(signatureB64);
-
- String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-
- private static String generateRandom32B64Url() {
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- return Base64Ws.encode(buf);
- }
-}
-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.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.GeoLookupService;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListSessions (v2) — список активных сессий.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей здесь больше нет.
- */
-public class Net_ListSessions_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListSessions_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- List sessions;
- try {
- sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
- } catch (SQLException e) {
- log.error("Ошибка БД при получении списка сессий для login={}", currentLogin, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_LIST_SESSIONS",
- "Ошибка доступа к базе данных при получении списка сессий"
- );
- }
-
- List resultList = new ArrayList<>();
- for (ActiveSessionEntry s : sessions) {
- SessionInfo info = new SessionInfo();
- info.setSessionId(s.getSessionId());
- info.setClientInfoFromClient(s.getClientInfoFromClient());
- info.setClientInfoFromRequest(s.getClientInfoFromRequest());
- info.setLastAuthirificatedAtMs(s.getLastAuthirificatedAtMs());
-
- String ip = s.getClientIp();
- String geo = GeoLookupService.resolveCountryCityOrIpWithCache(ip);
- info.setGeo(geo);
-
- resultList.add(info);
- }
-
- Net_ListSessions_Response resp = new Net_ListSessions_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessions(resultList);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-
-import java.security.SecureRandom;
-import java.sql.SQLException;
-
-/**
- * SessionChallenge (v2) — шаг 1 входа в существующую сессию.
- *
- * Логика авторизации (v2):
- * - Вход в существующую сессию ВСЕГДА в 2 шага:
- * 1) SessionChallenge(sessionId) -> nonce
- * 2) SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
- *
- * Что делает:
- * - Проверяет, что sessionId существует в БД.
- * - Генерирует одноразовый nonce (base64url(32)), сохраняет его в ctx:
- * ctx.sessionLoginNonce, ctx.sessionLoginSessionId, ctx.sessionLoginNonceExpiresAtMs.
- */
-public class Net_SessionChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
- private static final long NONCE_TTL_MS = 60_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionChallenge_Request req = (Net_SessionChallenge_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String nonce = Base64Ws.encode(buf);
-
- long now = System.currentTimeMillis();
- ctx.setSessionLoginNonce(nonce);
- ctx.setSessionLoginSessionId(sessionId);
- ctx.setSessionLoginNonceExpiresAtMs(now + NONCE_TTL_MS);
-
- Net_SessionChallenge_Response resp = new Net_SessionChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setNonce(nonce);
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.SQLException;
-
-/**
- * SessionLogin (v2) — шаг 2 входа в существующую сессию (по sessionKey).
- *
- * Логика авторизации (v2):
- * - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
- * - SessionLogin проверяет подпись sessionKey над строкой:
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- * - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
- *
- * При успехе:
- * - ctx становится AUTH_STATUS_USER
- * - обновляем метаданные сессии (lastAuth + clientIp + clientInfo + lang)
- * - возвращаем storagePwd
- */
-public class Net_SessionLogin_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
-
- private static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- // проверка челленджа
- if (ctx.getSessionLoginNonce() == null
- || ctx.getSessionLoginSessionId() == null
- || System.currentTimeMillis() > ctx.getSessionLoginNonceExpiresAtMs()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_CHALLENGE",
- "Нет активного SessionChallenge или nonce истёк"
- );
- }
-
- if (!sessionId.equals(ctx.getSessionLoginSessionId())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "SESSION_ID_MISMATCH",
- "nonce был выдан для другого sessionId"
- );
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая подпись"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- String sessionPubKeyB64 = session.getSessionKey(); // это pubKey (Base64(32))
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_SESSION_KEY",
- "В сессии не задан session_key"
- );
- }
-
- String nonce = ctx.getSessionLoginNonce();
-
- boolean sigOk;
- try {
- sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный Base64 для ключа/подписи"
- );
- }
-
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- }
-
- // сжигаем nonce
- ctx.setSessionLoginNonce(null);
- ctx.setSessionLoginSessionId(null);
- ctx.setSessionLoginNonceExpiresAtMs(0);
-
- // подтягиваем пользователя
- SolanaUserEntry user;
- try {
- user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_USER_LOOKUP",
- "Ошибка доступа к базе данных при получении пользователя"
- );
- }
-
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "USER_NOT_FOUND_FOR_SESSION",
- "Пользователь для данной сессии не найден"
- );
- }
-
- // обновление метаданных
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String clientIp = null;
- String clientInfoFromRequest = null;
- String userLanguage = null;
-
- if (ctx.getWsSession() != null) {
- clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
- clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
- userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
-
- if (clientIp != null && !clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- long now = System.currentTimeMillis();
- try {
- ActiveSessionsDAO.getInstance().updateOnRefresh(
- sessionId,
- now,
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
- } catch (SQLException e) {
- log.error("Ошибка БД при updateOnRefresh sessionId={}", sessionId, e);
- }
-
- session.setLastAuthirificatedAtMs(now);
- session.setClientIp(clientIp);
- session.setClientInfoFromClient(clientInfoFromClient);
- session.setClientInfoFromRequest(clientInfoFromRequest);
- session.setUserLanguage(userLanguage);
-
- // ctx
- ctx.setActiveSession(session);
- ctx.setSolanaUser(user);
- ctx.setSessionId(sessionId);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // ответ
- Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setStoragePwd(session.getStoragePwd());
- return resp;
- }
-
- private static boolean verifySessionLoginSignature(
- String sessionPubKeyB64,
- String sessionId,
- long timeMs,
- String nonce,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // pubKey: Base64(32). (Ed25519Util.keyFromBase64 должен использовать стандартный Base64)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);
-
- // signature: Base64(64) через единую утилиту WS-протокола
- byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
-
- String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-public final class Net_AddBlock_Request extends Net_Request {
-
- private String blockchainName; // обязателен
- private int blockNumber; // обязателен
- private String prevBlockHash; // HEX(64) или "" для нулевого
- private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public int getBlockNumber() { return blockNumber; }
- public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; }
-
- public String getPrevBlockHash() { return prevBlockHash; }
- public void setPrevBlockHash(String prevBlockHash) { this.prevBlockHash = prevBlockHash; }
-
- public String getBlockBytesB64() { return blockBytesB64; }
- public void setBlockBytesB64(String blockBytesB64) { this.blockBytesB64 = blockBytesB64; }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ:
- * - reasonCode (null если ok)
- * - serverLastGlobalNumber / serverLastGlobalHash
- */
-public final class Net_AddBlock_Response extends Net_Response {
-
- /** null если ok, иначе строка причины (bad_block_base64, user_not_found, и т.п.) */
- private String reasonCode;
-
- /** что сервер считает последним по глобальной цепочке */
- private int serverLastGlobalNumber;
- private String serverLastGlobalHash;
-
- public String getReasonCode() { return reasonCode; }
- public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
-
- 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; }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain;
-
-import blockchain.BchBlockEntry;
-import blockchain.BchCryptoVerifier;
-import blockchain.MsgSubType;
-import blockchain.body.BodyHasLine;
-import blockchain.body.BodyHasTarget;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ConnectionContext;
-import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainLocks;
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.BlocksDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.BlockEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.util.Arrays;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * Net_AddBlock_Handler — единый хэндлер добавления блока (JSON).
- *
- * Изменение (v3):
- * - ВСЕ ошибки теперь возвращаются в стандартном формате Net_Exception_Response:
- * status != 200, payload: { code, message, serverLastGlobalNumber, serverLastGlobalHash }
- * - Успех — как и раньше Net_AddBlock_Response (status=200).
- */
-public final class Net_AddBlock_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_Handler.class);
-
- private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
- private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) {
-
- Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;
-
- String blockchainName = req.getBlockchainName();
- ReentrantLock lock = BlockchainLocks.lockFor(blockchainName);
- lock.lock();
- try {
- AddBlockResult r = addBlock(
- blockchainName,
- req.getBlockNumber(), // старое поле, пока оставляем
- req.getPrevBlockHash(), // старое поле, пока оставляем
- req.getBlockBytesB64()
- );
-
- // ✅ УСПЕХ: как раньше
- if (r.isOk()) {
- Net_AddBlock_Response resp = new Net_AddBlock_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setReasonCode(null);
- resp.setServerLastGlobalNumber(r.serverLastBlockNumber);
- resp.setServerLastGlobalHash(r.serverLastBlockHashHex);
-
- return resp;
- }
-
- // ✅ ОШИБКА: стандартный формат (code + message) + доп.поля для ресинка
- return error(req, r.httpStatus, r.reasonCode, r.serverLastBlockNumber, r.serverLastBlockHashHex);
-
- } finally {
- lock.unlock();
- }
- }
-
- private Net_Response error(Net_AddBlock_Request req,
- int status,
- String reasonCode,
- int serverLastNum,
- String serverLastHashHex) {
-
- AddBlockExceptionResponse resp = new AddBlockExceptionResponse();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(status);
-
- // code — машинный
- resp.setCode(reasonCode != null ? reasonCode : "add_block_error");
- // message — человеческий (можешь улучшать тексты как угодно)
- resp.setMessage(humanMessage(reasonCode));
-
- // полезно клиенту для ресинка
- resp.setServerLastGlobalNumber(serverLastNum);
- resp.setServerLastGlobalHash(serverLastHashHex);
-
- return resp;
- }
-
- private static String humanMessage(String code) {
- if (code == null) return "Ошибка добавления блока";
-
- return switch (code) {
- case "empty_blockchain_name" -> "Пустое имя блокчейна";
- case "bad_blockchain_name" -> "Некорректное имя блокчейна";
- case "db_error" -> "Ошибка базы данных";
- case "blockchain_state_not_found" -> "Состояние блокчейна не найдено";
- case "state_last_hash_invalid" -> "Повреждено состояние блокчейна: неверный last_block_hash";
- case "bad_block_base64" -> "Некорректный base64 блока";
- case "limit_exceeded" -> "Превышен лимит размера блокчейна";
- case "limit_check_failed" -> "Ошибка проверки лимита размера";
- case "bad_block_format" -> "Некорректный формат блока";
- case "bad_block_body" -> "Некорректное тело блока";
- case "bad_block_number" -> "Некорректный номер блока";
- case "req_global_mismatch" -> "Номер блока в запросе не совпадает с номером в блоке";
- case "bad_prev_hash" -> "Некорректный prevHash (цепочка не совпадает)";
- case "bad_blockchain_key_len" -> "Некорректный ключ блокчейна в состоянии (ожидалось 32 байта)";
- case "signature_verify_failed" -> "Ошибка проверки подписи блока";
- case "bad_signature" -> "Некорректная подпись блока";
- case "prev_line_block_not_found" -> "Не найден блок prevLineNumber для проверки линии";
- case "bad_prev_line_hash" -> "Некорректный prevLineHash";
- case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
- case "internal_error" -> "Внутренняя ошибка сервера при записи блока";
- default -> "Ошибка: " + code;
- };
- }
-
- private AddBlockResult addBlock(
- String blockchainName,
- int globalNumberFromReq,
- String prevGlobalHashHexFromReq,
- String blockBytesB64
- ) {
- if (blockchainName == null || blockchainName.isBlank()) {
- log.warn("AddBlock: пустой blockchainName (reqGlobalNumber={})", globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
- }
-
- String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
- if (login == null || login.isBlank()) {
- log.warn("AddBlock: плохой blockchainName='{}' => login не получился (reqGlobalNumber={})",
- blockchainName, globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
- }
-
- // 1) state обязателен
- final BlockchainStateEntry st;
- try {
- st = stateDAO.getByBlockchainName(blockchainName);
- } catch (Exception e) {
- log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
- }
-
- if (st == null) {
- log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
- }
-
- final int serverLastNum = st.getLastBlockNumber();
-
- final byte[] serverLastHash32;
- try {
- serverLastHash32 = (serverLastNum < 0)
- ? new byte[32]
- : require32OrThrow(st.getLastBlockHash(), "state.last_block_hash is null/invalid");
- } catch (Exception e) {
- // ✅ Раньше тут мог вылететь неожиданный 500 через внешний try/catch.
- log.error("AddBlock: state_last_hash_invalid (login={}, blockchainName={}, serverLastNum={})",
- login, blockchainName, serverLastNum, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "state_last_hash_invalid", serverLastNum, "");
- }
-
- final String serverLastHashHex = toHex(serverLastHash32);
-
- // 2) decode block
- final byte[] blockBytes;
- try {
- blockBytes = decodeBase64(blockBytesB64);
- } catch (Exception e) {
- log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
- }
-
- // 3) лимит (оставляем как было)
- try {
- long oldSize = st.getFileSizeBytes();
- long limit = st.getSizeLimit();
- long newSize = safeAdd(oldSize, blockBytes.length);
-
- if (limit > 0 && newSize > limit) {
- log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, oldSize={}, addLen={}, newSize={}, limit={})",
- login, blockchainName, oldSize, blockBytes.length, newSize, limit);
- return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
- }
- } catch (Exception e) {
- log.error("AddBlock: limit_check_failed (login={}, blockchainName={})", login, blockchainName, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
- }
-
- // 4) parse block
- final BchBlockEntry block;
- try {
- block = new BchBlockEntry(blockBytes);
- } catch (Exception e) {
- log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, bytesLen={})",
- login, blockchainName, blockBytes.length, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
- }
-
- // body.check()
- try {
- block.body.check();
- } catch (Exception e) {
- log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, blockNumber={}, type={}, ver={})",
- login, blockchainName, block.blockNumber, (block.type & 0xFFFF), (block.version & 0xFFFF), e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
- }
-
- // 4.2) запрет дырок: blockNumber строго last+1
- int expectedBlockNumber = serverLastNum + 1;
- if (block.blockNumber != expectedBlockNumber) {
- log.warn("AddBlock: bad_block_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={})",
- login, blockchainName, block.blockNumber, expectedBlockNumber, serverLastNum);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_number", serverLastNum, serverLastHashHex);
- }
-
- // (временная совместимость) req.globalNumber должен совпасть с block.blockNumber
- if (globalNumberFromReq != block.blockNumber) {
- log.warn("AddBlock: req_global_mismatch (login={}, blockchainName={}, reqGlobal={}, blockNumber={})",
- login, blockchainName, globalNumberFromReq, block.blockNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "req_global_mismatch", serverLastNum, serverLastHashHex);
- }
-
- // 4.3) проверка цепочки по prevHash32
- if (!Arrays.equals(block.prevHash32, serverLastHash32)) {
- log.warn("AddBlock: bad_prev_hash (login={}, blockchainName={}, blockNumber={}, clientPrev={}, serverPrev={})",
- login, blockchainName, block.blockNumber, toHex(block.prevHash32), serverLastHashHex);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_hash", serverLastNum, serverLastHashHex);
- }
-
- // 5) pubKey
- final byte[] pubKey32 = st.getBlockchainKeyBytes();
- if (pubKey32 == null || pubKey32.length != 32) {
- log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, blockNumber={}, keyLen={})",
- login, blockchainName, block.blockNumber, (pubKey32 == null ? -1 : pubKey32.length));
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
- }
-
- // 6) подпись по hash32(preimage)
- boolean sigOk;
- try {
- sigOk = BchCryptoVerifier.verifyBlock(block, pubKey32);
- } catch (Exception e) {
- log.warn("AddBlock: signature_verify_failed (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "signature_verify_failed", serverLastNum, serverLastHashHex);
- }
-
- if (!sigOk) {
- log.warn("AddBlock: bad_signature (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
- }
-
- // 7) line columns (only for BodyHasLine)
- Integer lineCode = null;
- Integer prevLineNumber = null;
- byte[] prevLineHash32 = null;
- Integer thisLineNumber = null;
-
- if (block.body instanceof BodyHasLine bl) {
- lineCode = bl.lineCode();
- prevLineNumber = bl.prevLineBlockGlobalNumber();
- prevLineHash32 = bl.prevLineBlockHash32();
- thisLineNumber = bl.lineSeq();
-
- // Нормализация: -1 не пишем в БД (для совместимости со старым TextBody)
- if (prevLineNumber != null && prevLineNumber == -1) {
- prevLineNumber = null;
- prevLineHash32 = null;
- thisLineNumber = null;
- }
-
- // Если prevLineNumber задан — проверяем его хэш
- if (prevLineNumber != null) {
- try {
- byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
- if (dbPrevHash == null) {
- log.warn("AddBlock: prev_line_block_not_found (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
- login, blockchainName, block.blockNumber, prevLineNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_line_block_not_found", serverLastNum, serverLastHashHex);
- }
- if (!Arrays.equals(dbPrevHash, require32OrThrow(prevLineHash32, "prevLineHash32 invalid"))) {
- log.warn("AddBlock: bad_prev_line_hash (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
- login, blockchainName, block.blockNumber, prevLineNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_line_hash", serverLastNum, serverLastHashHex);
- }
- } catch (Exception e) {
- log.error("AddBlock: db_error_prev_line_check (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_prev_line_check", serverLastNum, serverLastHashHex);
- }
- }
- }
-
- // 8) сформировать запись и записать (DB + state + файл)
- try {
- BlockEntry be = new BlockEntry();
- be.setLogin(login);
- be.setBchName(blockchainName);
-
- be.setBlockNumber(block.blockNumber);
- be.setMsgType(block.type & 0xFFFF);
- be.setMsgSubType(block.subType & 0xFFFF);
-
- be.setBlockBytes(block.toBytes());
- be.setBlockHash(block.getHash32());
- be.setBlockSignature(block.getSignature64());
-
- // line columns (optional)
- be.setLineCode(lineCode);
- be.setPrevLineNumber(prevLineNumber);
- be.setPrevLineHash(prevLineHash32);
- be.setThisLineNumber(thisLineNumber);
-
- // target columns (optional)
- if (block.body instanceof BodyHasTarget t) {
- be.setToLogin(t.toLogin());
- be.setToBchName(t.toBchName());
- be.setToBlockNumber(t.toBlockGlobalNumber());
- be.setToBlockHash(t.toBlockHashBytes());
- }
-
- // edit helper (optional): если TEXT_EDIT_* — это "редактирование блока цели"
- int type = block.type & 0xFFFF;
- int sub = block.subType & 0xFFFF;
-
- if (type == 1
- && (sub == (MsgSubType.TEXT_EDIT_POST & 0xFFFF) || sub == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF))
- && be.getToBlockNumber() != null) {
- be.setEditedByBlockNumber(be.getToBlockNumber());
- }
-
- dbWriter.appendBlockAndState(blockchainName, block, st, be);
-
- } catch (Exception e) {
- log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
- }
-
- String newHashHex = toHex(block.getHash32());
-
- log.info("✅ AddBlock ok: login={}, blockchainName={}, blockNumber={}, newHash={}",
- login, blockchainName, block.blockNumber, newHashHex);
-
- return new AddBlockResult(WireCodes.Status.OK, null, block.blockNumber, newHashHex);
- }
-
- /* ===================================================================== */
- /* ====================== Helpers ====================================== */
- /* ===================================================================== */
-
- private static byte[] decodeBase64(String b64) {
- if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
- return Base64Ws.decode(b64);
- }
-
- private static long safeAdd(long a, long b) {
- long r = a + b;
- if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
- return r;
- }
-
- private static byte[] require32OrThrow(byte[] b, String msg) {
- if (b == null || b.length != 32) throw new IllegalArgumentException(msg);
- return b;
- }
-
- private static String toHex(byte[] bytes) {
- if (bytes == null) return "null";
- char[] HEX = "0123456789abcdef".toCharArray();
- char[] out = new char[bytes.length * 2];
- for (int i = 0; i < bytes.length; i++) {
- int v = bytes[i] & 0xFF;
- out[i * 2] = HEX[v >>> 4];
- out[i * 2 + 1] = HEX[v & 0x0F];
- }
- return new String(out);
- }
-
- /**
- * Спец-ответ ошибки AddBlock: стандартный code/message + поля для ресинка.
- * В wire-формате это окажется внутри payload.
- */
- public static final class AddBlockExceptionResponse extends Net_Exception_Response {
- private Integer serverLastGlobalNumber;
- private String serverLastGlobalHash;
-
- public Integer getServerLastGlobalNumber() {
- return serverLastGlobalNumber;
- }
-
- public void setServerLastGlobalNumber(Integer serverLastGlobalNumber) {
- this.serverLastGlobalNumber = serverLastGlobalNumber;
- }
-
- public String getServerLastGlobalHash() {
- return serverLastGlobalHash;
- }
-
- public void setServerLastGlobalHash(String serverLastGlobalHash) {
- this.serverLastGlobalHash = serverLastGlobalHash;
- }
- }
-
- private static final class AddBlockResult {
- final int httpStatus;
- final String reasonCode;
- final int serverLastBlockNumber;
- final String serverLastBlockHashHex;
-
- AddBlockResult(int httpStatus, String reasonCode, int serverLastBlockNumber, String serverLastBlockHashHex) {
- this.httpStatus = httpStatus;
- this.reasonCode = reasonCode;
- this.serverLastBlockNumber = serverLastBlockNumber;
- this.serverLastBlockHashHex = serverLastBlockHashHex;
- }
-
- boolean isOk() { return httpStatus == WireCodes.Status.OK; }
- }
-}
-
-package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.ReentrantLock;
-
-public final class BlockchainLocks {
- private static final ConcurrentHashMap MAP = new ConcurrentHashMap<>();
-
- private BlockchainLocks() {}
-
- public static ReentrantLock lockFor(String blockchainName) {
- return MAP.computeIfAbsent(blockchainName, id -> new ReentrantLock(true)); // fair=true
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
-
-import blockchain.BchBlockEntry;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.BlocksDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.BlockEntry;
-import utils.files.FileStoreUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-/**
- * BlockchainWriter — запись блока в DB + обновление state + запись в файл.
- *
- * ВАЖНО:
- * - Это минимальный рабочий вариант под новый формат.
- * - Если у тебя уже есть "атомарность" сложнее (tmp_bch + commit/recovery) — можно усилить потом.
- */
-public final class BlockchainWriter {
-
- private final BlocksDAO blocksDAO;
- private final BlockchainStateDAO stateDAO;
- private final FileStoreUtil fs = FileStoreUtil.getInstance();
-
- public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
- this.blocksDAO = blocksDAO;
- this.stateDAO = stateDAO;
- }
-
- public void appendBlockAndState(String blockchainName,
- BchBlockEntry block,
- BlockchainStateEntry st,
- BlockEntry be) throws SQLException {
-
- long nowMs = System.currentTimeMillis();
-
- try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
- c.setAutoCommit(false);
- try {
- // 1) insert block
- blocksDAO.insert(c, be);
-
- // 2) update state
- st.setLastBlockNumber(block.blockNumber);
- st.setLastBlockHash(block.getHash32());
- st.setFileSizeBytes(st.getFileSizeBytes() + block.toBytes().length);
- st.setUpdatedAtMs(nowMs);
-
- stateDAO.upsert(c, st);
-
- c.commit();
- } catch (Exception e) {
- try { c.rollback(); } catch (Exception ignored) {}
- if (e instanceof SQLException se) throw se;
- throw new SQLException("appendBlockAndState failed", e);
- } finally {
- try { c.setAutoCommit(true); } catch (Exception ignored) {}
- }
- }
-
- // 3) append to file (минимально: просто дописать)
- // Если у тебя уже есть логика tmp_bch+atomicReplace — можно заменить тут.
- String fileName = fs.buildBlockchainFileName(blockchainName);
- fs.addDataToFile(fileName, block.toBytes());
- }
-}
-package server.logic.ws_protocol.JSON.handlers.connections.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetFriendsLists — получить два списка "друзей" по connections_state.
- *
- * {
- * "op": "GetFriendsLists",
- * "requestId": "req-100",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * Возвращает:
- * - out_friends: кому login поставил FRIEND
- * - in_friends: кто поставил FRIEND этому login
- *
- * ПРО ДОСТУП (на будущее):
- * Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей.
- */
-public class Net_GetFriendsLists_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.connections.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ GetFriendsLists.
- *
- * {
- * "op": "GetFriendsLists",
- * "requestId": "req-100",
- * "status": 200,
- * "payload": {
- * "login": "Anya", // канонический регистр из БД
- * "out_friends": ["Bob", "Kate"], // кому login поставил FRIEND
- * "in_friends": ["Alex", "Kate"] // кто поставил FRIEND login
- * }
- * }
- */
-public class Net_GetFriendsLists_Response extends Net_Response {
-
- private String login;
-
- private List out_friends = new ArrayList<>();
- private List in_friends = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List getOut_friends() { return out_friends; }
- public void setOut_friends(List out_friends) { this.out_friends = out_friends; }
-
- public List getIn_friends() { return in_friends; }
- public void setIn_friends(List in_friends) { this.in_friends = in_friends; }
-}
-package server.logic.ws_protocol.JSON.handlers.connections;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.MsgSubType;
-import shine.db.SqliteDbController;
-import shine.db.dao.ConnectionsStateDAO;
-
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.util.List;
-
-/**
- * GetFriendsLists — получить 2 списка:
- * - out_friends: кому login поставил FRIEND
- * - in_friends: кто поставил FRIEND этому login
- *
- * ВАЖНО:
- * - login в запросе может быть любым регистром
- * - в ответе возвращаем канонический регистр (как в solana_users.login)
- *
- * ПРИМЕЧАНИЕ:
- * Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
- */
-public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- final String loginAnyCase = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
-
- // 1) Канонизируем login через solana_users (NOCASE)
- String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
- if (canonicalLogin == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- int relType = (int) MsgSubType.CONNECTION_FRIEND;
-
- // 2) Два списка (логины канонические)
- List outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType);
- List inFriends = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType);
-
- Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(canonicalLogin);
- resp.setOut_friends(outFriends);
- resp.setIn_friends(inFriends);
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetFriendsLists", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-
- private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
- String sql = """
- SELECT login
- FROM solana_users
- WHERE login = ? COLLATE NOCASE
- LIMIT 1
- """;
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, loginAnyCase);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return rs.getString("login");
- }
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers;
-
-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;
-
-/**
- * Общий интерфейс для всех JSON-хэндлеров.
- */
-public interface JsonMessageHandler {
-
- /**
- * Обработать запрос и вернуть ответ.
- *
- * @param request распарсенный запрос
- * @param ctx контекст текущего WebSocket-соединения
- */
- Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception;
-}
-
-package server.logic.ws_protocol.JSON.handlers.system.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Ping:
- * {
- * "op": "Ping",
- * "requestId": "req-1",
- * "payload": { "ts": 1700000000000 }
- * }
- *
- * Сервер ничего не проверяет, поле ts можно слать любое.
- */
-public class Net_Ping_Request extends Net_Request {
-
- private long ts;
-
- public long getTs() { return ts; }
- public void setTs(long ts) { this.ts = ts; }
-}
-package server.logic.ws_protocol.JSON.handlers.system.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Pong-ответ:
- * {
- * "op": "Ping",
- * "requestId": "req-1",
- * "status": 200,
- * "payload": { "ts": 1700000000123 }
- * }
- */
-public class Net_Ping_Response extends Net_Response {
-
- private long ts;
-
- public long getTs() { return ts; }
- public void setTs(long ts) { this.ts = ts; }
-}
-package server.logic.ws_protocol.JSON.handlers.system;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
-import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Response;
-import server.logic.ws_protocol.WireCodes;
-
-/**
- * Ping — keep-alive.
- * В ответ кладём только ts (текущее время сервера в мс).
- */
-public class Net_Ping_Handler implements JsonMessageHandler {
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_Ping_Request req = (Net_Ping_Request) baseRequest;
-
- Net_Ping_Response resp = new Net_Ping_Response();
- resp.setOp(req.getOp()); // "Ping"
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- // ничего не проверяем, просто отдаём серверное время
- resp.setTs(System.currentTimeMillis());
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос AddUser — временная/тестовая регистрация локального пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "payload": {
- * "login": "anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "base64-ed25519-public-key-login",
- * "blockchainKey": "base64-ed25519-public-key-blockchain",
- * "deviceKey": "base64-ed25519-public-key-device",
- * "bchLimit": 1000000
- * }
- * }
- *
- * Все поля лежат внутри payload.
- */
-public class Net_AddUser_Request extends Net_Request {
-
- private String login;
- private String blockchainName;
-
- /** Ключ пользователя Solana (публичный ключ логина) */
- private String solanaKey;
-
- /** Ключ блокчейна (публичный ключ блокчейна) */
- private String blockchainKey;
-
- /** Ключ устройства (публичный ключ устройства) */
- private String deviceKey;
-
- private Integer bchLimit;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-
- public Integer getBchLimit() { return bchLimit; }
- public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
-}
-// file: server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Успешный ответ на AddUser.
- *
- * Сейчас дополнительных полей нет — достаточно status=200.
- *
- * Пример:
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_AddUser_Response extends Net_Response {
- // При необходимости сюда можно добавить, например, флаг created/updated и т.п.
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUser — проверка/получение пользователя по login.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "payload": {
- * "login": "AnYa"
- * }
- * }
- *
- * Поиск по login выполняется без учёта регистра.
- * В ответе возвращаем login/blockchainName с тем регистром, как в БД.
- */
-public class Net_GetUser_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUser.
- *
- * Всегда status=200.
- *
- * Пример (нет пользователя):
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "status": 200,
- * "payload": { "exists": false }
- * }
- *
- * Пример (есть пользователь):
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "status": 200,
- * "payload": {
- * "exists": true,
- * "login": "Anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "...",
- * "blockchainKey": "...",
- * "deviceKey": "..."
- * }
- * }
- */
-public class Net_GetUser_Response extends Net_Response {
-
- private Boolean exists;
-
- private String login;
- private String blockchainName;
- private String solanaKey;
- private String blockchainKey;
- private String deviceKey;
-
- public Boolean getExists() { return exists; }
- public void setExists(Boolean exists) { this.exists = exists; }
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос SearchUsers — поиск логинов по префиксу.
- *
- * Клиент отправляет:
- * {
- * "op": "SearchUsers",
- * "requestId": "su-1",
- * "payload": { "prefix": "any" }
- * }
- *
- * Поиск по prefix выполняется без учёта регистра.
- * В ответе возвращаем логины с тем регистром, как в БД.
- */
-public class Net_SearchUsers_Request extends Net_Request {
-
- private String prefix;
-
- public String getPrefix() { return prefix; }
- public void setPrefix(String prefix) { this.prefix = prefix; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ SearchUsers.
- *
- * Всегда status=200.
- *
- * Пример:
- * {
- * "op": "SearchUsers",
- * "requestId": "su-1",
- * "status": 200,
- * "payload": {
- * "logins": ["Anya", "andrew", "Angel"]
- * }
- * }
- */
-public class Net_SearchUsers_Response extends Net_Response {
-
- private List logins = new ArrayList<>();
-
- public List getLogins() { return logins; }
- public void setLogins(List logins) { this.logins = logins; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.SolanaUserEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-public class Net_AddUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
-
- /** TEST ONLY */
- private static final int TEST_BCH_LIMIT = 1_000_000;
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getBlockchainName() == null || req.getBlockchainName().isBlank()
- || req.getSolanaKey() == null || req.getSolanaKey().isBlank()
- || req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
- || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
- );
- }
-
- // blockchainName должен быть вида: -NNN
- if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BLOCKCHAIN_NAME",
- "blockchainName должен быть вида -NNN (пример: anya-001)"
- );
- }
-
- int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0)
- ? TEST_BCH_LIMIT
- : req.getBchLimit();
-
- try {
- // базовая валидация форматов ключей: Base64(32 bytes)
- byte[] solanaKey32;
- byte[] blockchainKey32;
- byte[] deviceKey32;
-
- try {
- solanaKey32 = Base64Ws.decodeLen(req.getSolanaKey(), 32, "solanaKey");
- blockchainKey32 = Base64Ws.decodeLen(req.getBlockchainKey(), 32, "blockchainKey");
- deviceKey32 = Base64Ws.decodeLen(req.getDeviceKey(), 32, "deviceKey");
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_KEY_FORMAT",
- e.getMessage()
- );
- }
-
- // (переменные не используются дальше, но оставляем для ясности проверки длины)
- if (solanaKey32.length != 32 || blockchainKey32.length != 32 || deviceKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_KEY_FORMAT",
- "solanaKey/blockchainKey/deviceKey должны быть Base64(32 bytes)"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- SqliteDbController db = SqliteDbController.getInstance();
-
- try (Connection c = db.getConnection()) {
- c.setAutoCommit(false);
-
- // 1. Проверяем, что пользователя нет (case-insensitive)
- if (usersDAO.getByLogin(c, req.getLogin()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "USER_ALREADY_EXISTS",
- "Пользователь с таким login уже существует"
- );
- }
-
- // 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД)
- if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_ALREADY_EXISTS",
- "Пользователь с таким blockchainName уже существует"
- );
- }
-
- // 3. На всякий случай оставляем старую проверку blockchain_state,
- // потому что эта таблица нужна серверу (состояние цепочки/лимиты).
- if (stateDAO.getByBlockchainName(c, req.getBlockchainName()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_STATE_ALREADY_EXISTS",
- "blockchain_state уже существует"
- );
- }
-
- // 4. Создаём пользователя (все поля теперь лежат в solana_users)
- SolanaUserEntry user = new SolanaUserEntry();
- user.setLogin(req.getLogin());
- user.setBlockchainName(req.getBlockchainName());
- user.setSolanaKey(req.getSolanaKey());
- user.setBlockchainKey(req.getBlockchainKey());
- user.setDeviceKey(req.getDeviceKey());
-
- usersDAO.insert(c, user);
-
- // 5. Создаём INITIAL blockchain_state (для работы сервера)
- BlockchainStateEntry st = new BlockchainStateEntry();
- st.setBlockchainName(req.getBlockchainName());
- st.setLogin(req.getLogin());
- st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
- st.setLastBlockNumber(-1);
- st.setLastBlockHash(new byte[32]);
- st.setFileSizeBytes(0);
- st.setSizeLimit(limit);
- st.setUpdatedAtMs(System.currentTimeMillis());
-
- stateDAO.upsert(c, st);
-
- c.commit();
- }
-
- Net_AddUser_Response resp = new Net_AddUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
- req.getLogin(), req.getBlockchainName(), limit);
-
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-public class Net_GetUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUser_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUser_Request req = (Net_GetUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- // тут логичнее BAD_REQUEST, но ты просил: "нет пользователя" тоже 200.
- // Поэтому BAD_REQUEST оставляем только на реально пустой login.
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
-
- try {
- SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
-
- Net_GetUser_Response resp = new Net_GetUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (u == null) {
- resp.setExists(false);
- log.info("ℹ️ GetUser: not found for login={}", req.getLogin());
- return resp;
- }
-
- // ВАЖНО:
- // - Поиск по login был case-insensitive,
- // - а тут возвращаем login/blockchainName как в БД (с исходным регистром).
- resp.setExists(true);
- resp.setLogin(u.getLogin());
- resp.setBlockchainName(u.getBlockchainName());
- resp.setSolanaKey(u.getSolanaKey());
- resp.setBlockchainKey(u.getBlockchainKey());
- resp.setDeviceKey(u.getDeviceKey());
-
- log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName());
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error GetUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error GetUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class Net_SearchUsers_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SearchUsers_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_SearchUsers_Request req = (Net_SearchUsers_Request) baseRequest;
-
- if (req.getPrefix() == null || req.getPrefix().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: prefix"
- );
- }
-
- String prefix = req.getPrefix().trim();
-
- try {
- SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
- List users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5
-
- List logins = new ArrayList<>();
- for (SolanaUserEntry u : users) {
- if (u != null && u.getLogin() != null) {
- logins.add(u.getLogin()); // регистр как в БД
- }
- }
-
- Net_SearchUsers_Response resp = new Net_SearchUsers_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setLogins(logins);
-
- log.info("✅ SearchUsers ok: prefix='{}' -> {}", prefix, logins.size());
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error SearchUsers", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error SearchUsers", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUserParam — получить один параметр пользователя.
- *
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) этот запрос не ограничивает просмотр параметров, т.к. проект в тестовом режиме.
- * Позже, вероятно, потребуется ограничить: кто и какие параметры может читать (сессия/права).
- * Но для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUserParam.
- *
- * Если найден:
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * }
- * }
- *
- * Если не найден:
- * status=404, payload пустой.
- */
-public class Net_GetUserParam_Response extends Net_Response {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListUserParams — получить все сохранённые параметры пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ ListUserParams — список всех параметров пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "params": [
- * {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * },
- * ...
- * ]
- * }
- * }
- */
-public class Net_ListUserParams_Response extends Net_Response {
-
- private String login;
- private List
- params = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List
- getParams() { return params; }
- public void setParams(List
- params) { this.params = params; }
-
- public static class Item {
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-ed25519-public-key-32",
- * "signature": "base64-ed25519-signature-64"
- * }
- * }
- *
- * Подпись считается от UTF-8 строки:
- * USER_PARAMETER_PREFIX + login + param + time_ms + value
- */
-public class Net_UpsertUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
-
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на UpsertUserParam.
- *
- * Успех:
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_UpsertUserParam_Response extends Net_Response {
- // MVP: без payload. При желании позже можно добавить created/updated.
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-
-/**
- * GetUserParam — получить один параметр пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUserParam_Request req = (Net_GetUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param"
- );
- }
-
- String login = req.getLogin().trim();
- String param = req.getParam().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- UserParamEntry e = dao.getByLoginAndParam(c, login, param);
-
- if (e == null) {
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(404);
- return resp;
- }
-
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(e.getLogin());
- resp.setParam(e.getParam());
- resp.setTime_ms(e.getTimeMs());
- resp.setValue(e.getValue());
- resp.setDevice_key(e.getDeviceKey());
- resp.setSignature(e.getSignature());
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListUserParams — получить все параметры пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListUserParams_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_ListUserParams_Request req = (Net_ListUserParams_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- String login = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- List entries;
- try (Connection c = db.getConnection()) {
- entries = dao.getByLogin(c, login);
- }
-
- Net_ListUserParams_Response resp = new Net_ListUserParams_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(login);
-
- List items = new ArrayList<>();
- for (UserParamEntry e : entries) {
- Net_ListUserParams_Response.Item it = new Net_ListUserParams_Response.Item();
- it.setLogin(e.getLogin());
- it.setParam(e.getParam());
- it.setTime_ms(e.getTimeMs());
- it.setValue(e.getValue());
- it.setDevice_key(e.getDeviceKey());
- it.setSignature(e.getSignature());
- items.add(it);
- }
- resp.setParams(items);
-
- return resp;
-
- } catch (Exception e) {
- log.error("❌ Internal error ListUserParams", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.SolanaUserEntry;
-import shine.db.entities.UserParamEntry;
-import utils.config.ShineSignatureConstants;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.Connection;
-import java.sql.SQLException;
-
-/**
- * Net_UpsertUserParam_Handler
- *
- * Делает (MVP, без "сессий"):
- * 1) Проверка входных полей.
- * 2) Проверка подписи Ed25519 по device_key.
- * 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
- * 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
- *
- * ВАЖНО:
- * - НИКАКИХ ручных транзакций / BEGIN здесь нет.
- * - autoCommit=true, каждый statement завершённый сам по себе.
- * - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
- * наш финальный UPSERT просто вернёт 0 обновлённых строк.
- */
-public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()
- || req.getTime_ms() == null || req.getTime_ms() <= 0
- || req.getValue() == null
- || req.getDevice_key() == null || req.getDevice_key().isBlank()
- || req.getSignature() == null || req.getSignature().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param/time_ms/value/device_key/signature"
- );
- }
-
- final String login = req.getLogin().trim();
- final String param = req.getParam().trim();
- final long timeMs = req.getTime_ms();
- final String value = req.getValue();
- final String deviceKeyB64 = req.getDevice_key().trim();
- final String signatureB64 = req.getSignature().trim();
-
- try {
- // ---------------- Base64 decode ----------------
- byte[] pubKey32;
- byte[] sig64;
- try {
- pubKey32 = Base64Ws.decodeLen(deviceKeyB64, 32, "device_key");
- sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "device_key/signature должны быть Base64"
- );
- }
-
- // ---------------- Signature verify ----------------
- String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
- + login
- + param
- + timeMs
- + value;
-
- byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
-
- boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "SIGNATURE_INVALID",
- "Подпись не прошла проверку"
- );
- }
-
- // ---------------- DB checks + upsert ----------------
- SqliteDbController db = SqliteDbController.getInstance();
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- // 1) user exists
- SolanaUserEntry user = usersDAO.getByLogin(c, login);
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- // 2) device key must match the user's stored deviceKey
- String userDeviceKey = user.getDeviceKey();
- if (userDeviceKey == null || userDeviceKey.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "USER_DEVICE_KEY_EMPTY",
- "У пользователя не задан deviceKey в БД"
- );
- }
-
- if (!userDeviceKey.trim().equals(deviceKeyB64)) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "DEVICE_KEY_MISMATCH",
- "device_key не соответствует пользователю"
- );
- }
-
- // 3) atomic upsert-if-newer
- UserParamEntry e = new UserParamEntry(
- login,
- param,
- timeMs,
- value,
- deviceKeyB64,
- signatureB64
- );
-
- int changed = paramsDAO.upsertIfNewer(c, e);
-
- Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (changed == 1) {
- log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
- } else {
- // 0 строк — значит в БД уже есть time_ms >= incoming
- log.info("ℹ️ UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
- }
-
- return resp;
- }
-
- } catch (SQLException e) {
- log.error("❌ DB error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-
-import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
-
-// --- NEW v2 session login ---
-import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionChallenge_Handler;
-import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionLogin_Handler;
-
-// --- auth entities ---
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
-
-// --- NEW v2 entities ---
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
-
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
-
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
-
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
-
-// --- NEW: SearchUsers ---
-import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_SearchUsers_Handler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Request;
-
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_GetUserParam_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Handler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
-
-// --- NEW: connections friends lists ---
-import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
-
-// --- NEW: Ping ---
-import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler;
-import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
-
-import java.util.Map;
-
-/**
- * JsonHandlerRegistry — единое место, где руками регистрируются
- * JSON-операции: op → handler и op → requestClass.
- */
-public final class JsonHandlerRegistry {
-
- private static final Map HANDLERS = Map.ofEntries(
- Map.entry("AddUser", new Net_AddUser_Handler()),
- Map.entry("GetUser", new Net_GetUser_Handler()),
- Map.entry("SearchUsers", new Net_SearchUsers_Handler()),
-
- // --- auth ---
- Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
- Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()),
- Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
- Map.entry("ListSessions", new Net_ListSessions_Handler()),
-
- // --- login to existing session in 2 steps ---
- Map.entry("SessionChallenge", new Net_SessionChallenge_Handler()),
- Map.entry("SessionLogin", new Net_SessionLogin_Handler()),
-
- // --- blockchain ---
- Map.entry("AddBlock", new Net_AddBlock_Handler()),
-
- // --- userParams ---
- Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()),
- Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
- Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
-
- // --- connections ---
- Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
-
- // --- system ---
- Map.entry("Ping", new Net_Ping_Handler())
-
- // --- subscriptions ---
-// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
- );
-
- private static final Map> REQUEST_TYPES = Map.ofEntries(
- Map.entry("AddUser", Net_AddUser_Request.class),
- Map.entry("GetUser", Net_GetUser_Request.class),
- Map.entry("SearchUsers", Net_SearchUsers_Request.class),
-
- // --- auth ---
- Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),
- Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class),
- Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
- Map.entry("ListSessions", Net_ListSessions_Request.class),
-
- // --- NEW v2 ---
- Map.entry("SessionChallenge", Net_SessionChallenge_Request.class),
- Map.entry("SessionLogin", Net_SessionLogin_Request.class),
-
- // --- blockchain ---
- Map.entry("AddBlock", Net_AddBlock_Request.class),
-
- // --- userParams ---
- Map.entry("UpsertUserParam", Net_UpsertUserParam_Request.class),
- Map.entry("GetUserParam", Net_GetUserParam_Request.class),
- Map.entry("ListUserParams", Net_ListUserParams_Request.class),
-
- // --- connections ---
- Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
-
- // --- system ---
- Map.entry("Ping", Net_Ping_Request.class)
- );
-
- private JsonHandlerRegistry() { }
-
- public static Map getHandlers() {
- return HANDLERS;
- }
-
- public static Map> getRequestTypes() {
- return REQUEST_TYPES;
- }
-}
-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.Net_Exception_Response;
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-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-сообщений.
- *
- * 1) Парсит общий пакет (op, requestId, payload).
- * 2) По op выбирает класс запроса и хэндлер.
- * 3) Собирает "плоский" объект: op + requestId + поля из payload.
- * 4) Маппит его в NetRequest через ObjectMapper.
- * 5) Вызывает хэндлер, получает NetResponse.
- * 6) Собирает 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()
- .setSerializationInclusion(JsonInclude.Include.NON_NULL);
-
- private static final Map JSON_HANDLERS =
- JsonHandlerRegistry.getHandlers();
-
- private static final Map> JSON_REQUEST_TYPES =
- JsonHandlerRegistry.getRequestTypes();
-
- private JsonInboundProcessor() {
- // utility
- }
-
- public static String processJson(String json, ConnectionContext ctx) {
- String op = null;
- String requestId = null;
-
- // Для лога полезно знать, кто прислал (хотя бы login/sessionId, если есть)
- String ctxLogin = safe(ctx != null ? ctx.getLogin() : null);
- String ctxSessionId = safe(ctx != null ? ctx.getSessionId() : null);
-
- try {
- if (json == null || json.isBlank()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- null,
- null,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_JSON",
- "Пустое JSON-сообщение"
- );
-
- String out = writeResponse(err);
-
- // DEBUG: что пришло / что ушло
- if (log.isDebugEnabled()) {
- log.debug("JSON IN (login={}, sessionId={}): ", ctxLogin, ctxSessionId);
- log.debug("JSON OUT (login={}, sessionId={}): {}", ctxLogin, ctxSessionId, shorten(out, 1200));
- }
- return out;
- }
-
- // DEBUG: сырой вход (обрезаем, чтобы не убить лог)
- if (log.isDebugEnabled()) {
- log.debug("JSON IN (login={}, sessionId={}): {}", ctxLogin, ctxSessionId, shorten(json, 1200));
- }
-
- // 1) Парсим общий пакет
- JsonNode root = JSON_MAPPER.readTree(json);
-
- // 2) op и requestId из корня
- op = getTextOrNull(root, "op");
- requestId = getTextOrNull(root, "requestId");
-
- if (op == null || op.isEmpty()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- null,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "NO_OP",
- "Поле 'op' отсутствует или пустое"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- JsonMessageHandler handler = JSON_HANDLERS.get(op);
- Class extends Net_Request> reqClass = JSON_REQUEST_TYPES.get(op);
-
- if (handler == null || reqClass == null) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "UNKNOWN_OP",
- "Неизвестная операция: " + op
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // 3) Берём payload
- JsonNode payloadNode = root.get("payload");
- if (payloadNode == null || payloadNode.isNull()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "NO_PAYLOAD",
- "Поле 'payload' отсутствует"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
- if (!payloadNode.isObject()) {
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "BAD_PAYLOAD",
- "Поле 'payload' должно быть объектом"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // 3.1 Собираем "плоский" объект для маппинга в NetRequest:
- // op + requestId + поля из payload
- ObjectNode merged = JSON_MAPPER.createObjectNode();
-
- // Добавляем op и requestId, чтобы они попали в NetRequest
- merged.put("op", op);
- if (requestId != null) merged.put("requestId", requestId);
-
- // Добавляем все поля из payload внутрь
- merged.setAll((ObjectNode) payloadNode);
-
- // 4) Маппим в конкретный класс NetRequest
- Net_Request request;
- try {
- request = JSON_MAPPER.treeToValue(merged, reqClass);
- } catch (Exception mapErr) {
- // Важно: вот это часто “теряется”, если не логировать отдельно
- log.error("❌ JSON map error (op={}, requestId={}, login={}, sessionId={}): merged={}",
- op, safe(requestId), ctxLogin, ctxSessionId, shorten(merged.toString(), 1200), mapErr);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.BAD_REQUEST,
- "BAD_REQUEST_FORMAT",
- "Некорректный формат запроса: не удалось распарсить поля payload"
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // DEBUG: нормализованный запрос (уже распарсен)
- if (log.isDebugEnabled()) {
- log.debug("REQ OBJ (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(safeToString(request), 1200));
- }
-
- // 5) Вызываем хэндлер
- Net_Response response;
- try {
- response = handler.handle(request, ctx);
- } catch (Exception handlerError) {
- // ✅ Вот тут как раз и должны “появляться ошибки в логере”
- log.error("💥 Handler error (op={}, requestId={}, login={}, sessionId={})",
- op, safe(requestId), ctxLogin, ctxSessionId, handlerError);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op,
- requestId,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_HANDLER_ERROR",
- "Неожиданная ошибка при обработке операции: " + op
- );
-
- String out = writeResponse(err);
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
- return out;
- }
-
- // На всякий случай: если хэндлер не выставил op/requestId
- if (response.getOp() == null) response.setOp(op);
- if (response.getRequestId() == null) response.setRequestId(requestId);
-
- // 6) Универсальная сборка ответа
- String out = writeResponse(response);
-
- // DEBUG: ответ ушёл
- if (log.isDebugEnabled()) {
- log.debug("RESP OBJ (login={}, sessionId={}, op={}, requestId={}, status={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), response.getStatus(), shorten(safeToString(response), 1200));
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}, status={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), response.getStatus(), shorten(out, 1200));
- }
-
- return out;
-
- } catch (Exception e) {
- // ✅ Любая неожиданная ошибка парсинга/обработки — в лог
- log.error("❌ JSON processing error (op={}, requestId={}, login={}, sessionId={})",
- safe(op), safe(requestId), safe(ctxLogin), safe(ctxSessionId), e);
-
- Net_Exception_Response err = NetExceptionResponseFactory.error(
- op != null ? op : "Unknown",
- requestId,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
-
- String out = writeResponse(err);
-
- if (log.isDebugEnabled()) {
- log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
- ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
- }
-
- return out;
- }
- }
-
- // --- helpers ---
-
- private static String getTextOrNull(JsonNode node, String field) {
- if (node == null || !node.has(field) || node.get(field).isNull()) return null;
- return node.get(field).asText();
- }
-
- /**
- * Унифицированная сериализация любого NetResponse в формат:
- * {
- * "op": ...,
- * "requestId": ...,
- * "status": ...,
- * "payload": { ... }
- * }
- */
- private static String writeResponse(Net_Response response) {
- try {
- // Конвертируем полный объект ответа в 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);
-
- // payload — это всё, что осталось от full (может быть пустым объектом {})
- root.set("payload", full);
-
- return JSON_MAPPER.writeValueAsString(root);
-
- } catch (Exception e) {
- // Совсем аварийный случай — сериализация ответа сломалась.
- log.error("❌ Response serialization error (op={}, requestId={})",
- safe(response != null ? response.getOp() : null),
- safe(response != null ? response.getRequestId() : null),
- e);
-
- return "{\"op\":\"" + safe(response != null ? response.getOp() : null) +
- "\",\"requestId\":\"" + safe(response != null ? response.getRequestId() : null) +
- "\",\"status\":" + (response != null ? response.getStatus() : 500) +
- ",\"payload\":{\"code\":\"SERIALIZATION_ERROR\",\"message\":\"Ошибка сериализации ответа\"}}";
- }
- }
-
- private static String safe(String s) {
- return s != null ? s : "";
- }
-
- private static String shorten(String s, int max) {
- if (s == null) return "";
- if (s.length() <= max) return s;
- return s.substring(0, Math.max(0, max)) + "...(+" + (s.length() - max) + " chars)";
- }
-
- private static String safeToString(Object o) {
- if (o == null) return "null";
- try {
- // Чтобы не плодить огромные логи и не утыкаться в циклические ссылки —
- // логируем как JSON, если возможно.
- return JSON_MAPPER.writeValueAsString(o);
- } catch (Exception ignore) {
- return String.valueOf(o);
- }
- }
-}
-////package server.logic.ws_protocol.JSON.utils;
-//
-//import shine.db.entities.SolanaUserEntry;
-//import utils.crypto.Ed25519Util;
-//
-//import java.nio.charset.StandardCharsets;
-//import java.util.Base64;
-//
-//public final class AuthSignatures {
-//
-// private AuthSignatures() {}
-//
-// /** preimage для CreateAuthSession(v2): "AUTH_CREATE_SESSION:login:timeMs:authNonce" */
-// public static byte[] preimageCreateAuthSession(String login, long timeMs, String authNonce) {
-// String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
-// return preimageStr.getBytes(StandardCharsets.UTF_8);
-// }
-//
-// /** Декод base64 / base64url (если надо — подстрой под твой decodeBase64Any) */
-// public static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
-// if (s == null) throw new IllegalArgumentException("base64 is null");
-// String x = s.trim();
-// if (x.isEmpty()) throw new IllegalArgumentException("base64 is empty");
-//
-// try {
-// return Base64.getDecoder().decode(x);
-// } catch (IllegalArgumentException e1) {
-// // пробуем base64url без паддинга
-// return Base64.getUrlDecoder().decode(x);
-// }
-// }
-//
-// /**
-// * Проверка подписи CreateAuthSession(v2) по deviceKey пользователя.
-// * Подпись проверяется над preimageCreateAuthSession(...).
-// */
-// public static boolean verifyCreateAuthSessionSignature(
-// SolanaUserEntry user,
-// String login,
-// String authNonce,
-// long timeMs,
-// String signatureB64
-// ) throws IllegalArgumentException {
-//
-// // user.getDeviceKey() — base64 публичного ключа (32 байта)
-// byte[] publicKey32 = decodeBase64Any(user.getDeviceKey());
-// byte[] signature64 = decodeBase64Any(signatureB64);
-//
-// byte[] preimage = preimageCreateAuthSession(login, timeMs, authNonce);
-// return Ed25519Util.verify(preimage, signature64, publicKey32);
-// }
-//}
-package server.logic.ws_protocol.JSON.utils;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Фабрика ошибок для JSON-протокола.
- * Создаёт единообразные NetExceptionResponse.
- */
-public final class NetExceptionResponseFactory {
-
- private NetExceptionResponseFactory() {
- // запрет на создание объектов
- }
-
- public static Net_Exception_Response error(Net_Request req,
- int status,
- String code,
- String message) {
-
- Net_Exception_Response resp = new Net_Exception_Response();
-
- // ✅ НЕ падаем, даже если req == null
- if (req != null) {
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- } else {
- resp.setOp(null);
- resp.setRequestId(null);
- }
-
- resp.setStatus(status);
- resp.setCode(code);
- resp.setMessage(message);
- return resp;
- }
-
- /**
- * Вариант для случаев, когда NetRequest ещё не распарсен,
- * но мы уже знаем op и requestId (или они null).
- */
- public static Net_Exception_Response error(String op,
- String requestId,
- int status,
- String code,
- String message) {
-
- Net_Exception_Response resp = new Net_Exception_Response();
- resp.setOp(op);
- resp.setRequestId(requestId);
- resp.setStatus(status);
- resp.setCode(code);
- resp.setMessage(message);
- return resp;
- }
-}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/all_files.txt b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/all_files.txt
deleted file mode 100644
index 25f556f..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/all_files.txt
+++ /dev/null
@@ -1,140 +0,0 @@
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех событий (event).
- * Общие поля: op и payload.
- *.
- * Формат JSON (event):
- * {
- * "op": "...",
- * "payload": { ... }
- * }
- */
-public abstract class Net_Event {
-
- /** Имя операции / события (op). */
- private String op;
-
- /**
- * Произвольные данные.
- * В JSON это поле "payload".
- */
- private Object payload;
-
- // --- getters / setters ---
-
- public String getOp() {
- return op;
- }
-
- public void setOp(String op) {
- this.op = op;
- }
-
- public Object getPayload() {
- return payload;
- }
-
- public void setPayload(Object payload) {
- this.payload = payload;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Ответ с ошибкой (любой отказ).
- *.
- * В payload будет:
- * {
- * "code": "...",
- * "message": "..."
- * }
- */
-public class Net_Exception_Response extends Net_Response {
-
- 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;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех запросов (client → server).
- *.
- * Наследуется от NetEvent и добавляет requestId.
- *.
- * Формат JSON (request):
- * {
- * "op": "...",
- * "requestId": "...",
- * "payload": { ... }
- * }
- */
-public abstract class Net_Request extends Net_Event {
-
- /** Идентификатор запроса, чтобы связать запрос и ответ. */
- private String requestId;
-
- // --- getters / setters ---
-
- public String getRequestId() {
- return requestId;
- }
-
- public void setRequestId(String requestId) {
- this.requestId = requestId;
- }
-}
-
-package server.logic.ws_protocol.JSON.entyties;
-
-/**
- * Базовый класс для всех ответов (server → client).
- *.
- * Наследуется от NetRequest и добавляет status.
- *.
- * Формат JSON (response):
- * {
- * "op": "...",
- * "requestId": "...",
- * "status": 200,
- * "payload": { ... } // и для успеха, и для ошибки
- * }
- */
-public abstract class Net_Response extends Net_Request {
-
- /** Статус результата (200 — успех, любое другое значение — ошибка). */
- private int status;
-
- // --- getters / setters ---
-
- public int getStatus() {
- return status;
- }
-
- public void setStatus(int status) {
- this.status = status;
- }
-
- public boolean isOk() {
- return status == 200;
- }
-}
-
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/entyties/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/all_files.txt b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/all_files.txt
deleted file mode 100644
index 397359c..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/all_files.txt
+++ /dev/null
@@ -1,3475 +0,0 @@
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 авторизации: запрос выдачи одноразового nonce (authNonce).
- *
- * Клиент по логину просит сервер сгенерировать случайный authNonce,
- * который будет использован на втором шаге при подписи.
- *
- * Формат входящего JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "payload": {
- * "login": "someLogin"
- * }
- * }
- *
- * Формат успешного ответа:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Request extends Net_Request {
-
- /**
- * Логин пользователя, для которого запускается авторизация.
- */
- private String login;
-
- public String getLogin() {
- return login;
- }
- public void setLogin(String login) {
- this.login = login;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на AuthChallenge.
- *
- * При успехе сервер возвращает одноразовый nonce для подписи (authNonce),
- * который клиент обязан использовать на втором шаге при формировании строки
- * для цифровой подписи.
- *
- * JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Response extends Net_Response {
-
- /**
- * Одноразовый nonce для авторификации.
- * Строка — это base64-представление 32 случайных байт.
- */
- private String authNonce;
-
- public String getAuthNonce() {
- return authNonce;
- }
-
- public void setAuthNonce(String authNonce) {
- this.authNonce = authNonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос CloseActiveSession — закрытие активной сессии пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и "AUTH_IN_PROGRESS" здесь больше нет.
- *
- * payload:
- * {
- * "sessionId": "..." // опционально; если пусто — закрываем текущую
- * }
- */
-public class Net_CloseActiveSession_Request extends Net_Request {
-
- /** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CloseActiveSession.
- *
- * При успехе:
- * - status = 200;
- * - payload = {}.
- *
- * Закрытие WebSocket-соединения может быть выполнено сразу (для другой сессии)
- * или чуть позже (для текущей сессии) после отправки ответа.
- */
-public class Net_CloseActiveSession_Response extends Net_Response {
- // Дополнительных полей пока не требуется.
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
- *
- * Шаги:
- * 1) AuthChallenge(login) -> authNonce
- * 2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
- *
- * Подпись deviceKey делается над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
- *
- * Важно:
- * - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
- * - В БД active_sessions.session_key хранится sessionPubKeyB64.
- */
-public class Net_CreateAuthSession_Request extends Net_Request {
-
- /** Клиентский пароль для хранения данных (base64 от 32 байт). */
- private String storagePwd;
-
- /** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
- private String sessionPubKeyB64;
-
- /** Время на стороне клиента (мс с 1970-01-01). */
- private long timeMs;
-
- /** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-
- public String getSessionPubKeyB64() {
- return sessionPubKeyB64;
- }
-
- public void setSessionPubKeyB64(String sessionPubKeyB64) {
- this.sessionPubKeyB64 = sessionPubKeyB64;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CreateAuthSession (v2).
- *
- * При успехе сервер создаёт запись в active_sessions
- * и возвращает идентификатор сессии sessionId.
- *
- * JSON:
- * {
- * "op": "CreateAuthSession",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "sessionId": "base64(32)"
- * }
- * }
- */
-public class Net_CreateAuthSession_Response extends Net_Response {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListSessions — список активных сессий пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Пустой payload.
- */
-public class Net_ListSessions_Request extends Net_Request {
- // пусто
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.List;
-
-/**
- * Ответ на ListSessions.
- *
- * При успехе:
- * - status = 200;
- * - payload:
- * {
- * "sessions": [
- * {
- * "sessionId": "...",
- * "clientInfoFromClient": "...",
- * "clientInfoFromRequest": "...",
- * "geo": "Country, City" | "unknown",
- * "lastAuthirificatedAtMs": 1733310000000
- * },
- * ...
- * ]
- * }
- */
-public class Net_ListSessions_Response extends Net_Response {
-
- /**
- * Список активных сессий для текущего пользователя.
- */
- private List sessions;
-
- public List getSessions() {
- return sessions;
- }
-
- public void setSessions(List sessions) {
- this.sessions = sessions;
- }
-
- /**
- * Описание одной активной сессии.
- */
- public static class SessionInfo {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- /** Что прислал клиент в CreateAuthSession/RefreshSession (clientInfo). */
- private String clientInfoFromClient;
-
- /** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
- private String clientInfoFromRequest;
-
- /** Строка геолокации вида "Country, City" или "unknown". */
- private String geo;
-
- /** Время последней успешной авторизации/refresh (мс с 1970-01-01). */
- private long lastAuthirificatedAtMs;
-
- // --- getters / setters ---
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- public String getClientInfoFromClient() {
- return clientInfoFromClient;
- }
-
- public void setClientInfoFromClient(String clientInfoFromClient) {
- this.clientInfoFromClient = clientInfoFromClient;
- }
-
- public String getClientInfoFromRequest() {
- return clientInfoFromRequest;
- }
-
- public void setClientInfoFromRequest(String clientInfoFromRequest) {
- this.clientInfoFromRequest = clientInfoFromRequest;
- }
-
- public String getGeo() {
- return geo;
- }
-
- public void setGeo(String geo) {
- this.geo = geo;
- }
-
- public long getLastAuthirificatedAtMs() {
- return lastAuthirificatedAtMs;
- }
-
- public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
- this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 входа в существующую сессию (v2):
- * SessionChallenge(sessionId) -> nonce
- */
-public class Net_SessionChallenge_Request extends Net_Request {
-
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionChallenge (v2).
- * payload: { "nonce": "base64(32)" }
- */
-public class Net_SessionChallenge_Response extends Net_Response {
-
- private String nonce;
-
- public String getNonce() {
- return nonce;
- }
-
- public void setNonce(String nonce) {
- this.nonce = nonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 входа в существующую сессию (v2):
- * SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
- *
- * Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- *
- * nonce берётся из SessionChallenge и хранится в ctx (одноразовый, TTL).
- */
-public class Net_SessionLogin_Request extends Net_Request {
-
- private String sessionId;
- private long timeMs;
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionLogin (v2).
- * payload: { "storagePwd": "base64(32)" }
- */
-public class Net_SessionLogin_Response extends Net_Response {
-
- private String storagePwd;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.security.SecureRandom;
-
-/**
- * AuthChallenge (v2) — шаг 1 создания новой сессии.
- *
- * Логика авторизации (v2):
- * - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
- * - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
- * CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
- *
- * Что делает:
- * 1) Проверяет login.
- * 2) Находит пользователя (solana_users).
- * 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
- * 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
- */
-public class Net_AuthChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_AuthChallenge_Request req = (Net_AuthChallenge_Request) baseReq;
-
- String login = req.getLogin();
- if (login == null || login.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_LOGIN",
- "Пустой логин"
- );
- }
-
- // Если по этому соединению уже есть залогиненный пользователь — не даём повторную авторификацию
- if (ctx.getLogin() != null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "ALREADY_AUTHED",
- "Попытка повторной авторификации для уже заданного login=" + ctx.getLogin()
- );
- }
-
- SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
- if (solanaUserEntry == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "UNKNOWN_USER",
- "Пользователь с таким логином не найден"
- );
- }
-
- ctx.setSolanaUser(solanaUserEntry);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String authNonce = Base64Ws.encode(buf);
-
- ctx.setAuthNonce(authNonce);
-
- Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setAuthNonce(authNonce);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-/**
- * CloseActiveSession (v2) — закрытие текущей или другой сессии.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и AUTH_IN_PROGRESS здесь больше нет.
- *
- * Закрытие:
- * - удаляем запись из БД
- * - если по sessionId есть активный WS — закрываем его
- */
-public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- String targetSessionId = req.getSessionId();
- if (targetSessionId == null || targetSessionId.isBlank()) {
- if (ctx.getSessionId() != null && !ctx.getSessionId().isBlank()) {
- targetSessionId = ctx.getSessionId();
- } else if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
- targetSessionId = ctx.getActiveSession().getSessionId();
- } else {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_SESSION_TO_CLOSE",
- "Не удалось определить, какую сессию нужно закрыть"
- );
- }
- }
-
- ActiveSessionEntry targetSession;
- try {
- targetSession = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных при поиске сессии"
- );
- }
-
- if (targetSession == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия для закрытия не найдена"
- );
- }
-
- if (currentLogin == null || !currentLogin.equals(targetSession.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_OF_ANOTHER_USER",
- "Нельзя закрывать сессию другого пользователя"
- );
- }
-
- boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
-
- closeActiveSession(targetSessionId, ctx, isCurrentSession);
-
- Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- return resp;
- }
-
- private void closeActiveSession(String targetSessionId,
- ConnectionContext currentCtx,
- boolean isCurrentSession) {
-
- try {
- ActiveSessionsDAO.getInstance().deleteBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
- }
-
- ConnectionContext ctxToClose =
- ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
-
- if (ctxToClose == null) return;
-
- if (isCurrentSession && ctxToClose == currentCtx) {
- new Thread(() -> {
- try { Thread.sleep(50); } catch (InterruptedException ignored) {}
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }, "CloseSession-" + targetSessionId).start();
- } else {
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import org.eclipse.jetty.websocket.api.Session;
-
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.sql.SQLException;
-
-/**
- * CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
- *
- * Логика авторизации (v2):
- * - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
- * - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
- * отправляет на сервер ТОЛЬКО sessionPubKeyB64.
- * - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
- *
- * Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
- *
- * На выходе:
- * - создаётся запись active_sessions
- * - ctx становится AUTH_STATUS_USER (вход выполнен как "текущая сессия")
- * - ответ: sessionId
- */
-public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
- private static final SecureRandom RANDOM = new SecureRandom();
-
- public static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
-
- if (ctx == null
- || ctx.getSolanaUser() == null
- || ctx.getAuthNonce() == null
- || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
-
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_STEP1_CONTEXT",
- "Шаг 1 авторизации не был корректно выполнен для данного соединения"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no step1 context or bad auth state");
- return err;
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String login = user.getLogin();
- if (login == null || login.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_LOGIN",
- "Для пользователя не задан login в БД"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no login");
- return err;
- }
-
- String storagePwd = req.getStoragePwd();
- if (storagePwd == null || storagePwd.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_STORAGE_PWD",
- "Пустой storagePwd"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty storagePwd");
- return err;
- }
-
- String sessionPubKeyB64 = req.getSessionPubKeyB64();
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_PUBKEY",
- "Пустой sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
- return err;
- }
-
- // Проверим, что sessionPubKeyB64 декодируется в 32 байта
- byte[] sessionPubKey32;
- try {
- sessionPubKey32 = Base64Ws.decode(sessionPubKeyB64);
- } catch (IllegalArgumentException e) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный base64 в sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
- return err;
- }
- if (sessionPubKey32.length != 32) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_SESSION_PUBKEY_LEN",
- "sessionPubKey должен быть 32 байта"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
- return err;
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая цифровая подпись"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty signature");
- return err;
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- long diff = Math.abs(nowMs - timeMs);
- if (diff > ALLOWED_SKEW_MS) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: time skew");
- return err;
- }
-
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String devicePubKeyB64 = user.getDeviceKey();
- if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_DEVICE_KEY",
- "Отсутствует deviceKey у пользователя"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no deviceKey");
- return err;
- }
-
- String authNonce = ctx.getAuthNonce();
-
- boolean sigOk;
- try {
- sigOk = verifyCreateSessionSignature(
- user,
- login,
- authNonce,
- timeMs,
- signatureB64
- );
- } catch (IllegalArgumentException ex) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный формат Base64 для ключа или подписи"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad base64");
- return err;
- }
-
- if (!sigOk) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad signature");
- return err;
- }
-
- // --- генерируем sessionId ---
- String sessionId = generateRandom32B64Url();
- long now = System.currentTimeMillis();
-
- // --- Сбор данных о клиенте (IP, UA, язык) ---
- Session wsSession = ctx.getWsSession();
- String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
- String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
-
- String clientIp = "";
- if (wsSession != null) {
- String ip = ClientInfoService.extractClientIp(wsSession);
- if (ip != null) clientIp = ip;
-
- if (!clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- // --- создаём запись ActiveSession и сохраняем в БД ---
- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
- ActiveSessionEntry activeSessionEntry;
-
- try {
- activeSessionEntry = new ActiveSessionEntry(
- sessionId,
- login,
- sessionPubKeyB64, // session_key (pubkey)
- storagePwd,
- now,
- now,
- null, // pushEndpoint
- null, // pushP256dhKey
- null, // pushAuthKey
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
-
- dao.insert(activeSessionEntry);
- } catch (SQLException e) {
- log.error("Ошибка БД при создании новой сессии для login={}", login, e);
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_SESSION_CREATE",
- "Ошибка БД при создании сессии"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db error");
- return err;
- }
-
- // --- обновляем контекст ---
- ctx.setActiveSession(activeSessionEntry);
- ctx.setSessionId(sessionId);
- ctx.setAuthNonce(null);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // --- формируем ответ ---
- Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessionId(sessionId);
- return resp;
- }
-
- private static boolean verifyCreateSessionSignature(
- SolanaUserEntry user,
- String login,
- String authNonce,
- long timeMs,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // deviceKey (pub, 32)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
- byte[] signature64 = Base64Ws.decode(signatureB64);
-
- String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-
- private static String generateRandom32B64Url() {
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- return Base64Ws.encode(buf);
- }
-}
-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.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.GeoLookupService;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListSessions (v2) — список активных сессий.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей здесь больше нет.
- */
-public class Net_ListSessions_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListSessions_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- List sessions;
- try {
- sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
- } catch (SQLException e) {
- log.error("Ошибка БД при получении списка сессий для login={}", currentLogin, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_LIST_SESSIONS",
- "Ошибка доступа к базе данных при получении списка сессий"
- );
- }
-
- List resultList = new ArrayList<>();
- for (ActiveSessionEntry s : sessions) {
- SessionInfo info = new SessionInfo();
- info.setSessionId(s.getSessionId());
- info.setClientInfoFromClient(s.getClientInfoFromClient());
- info.setClientInfoFromRequest(s.getClientInfoFromRequest());
- info.setLastAuthirificatedAtMs(s.getLastAuthirificatedAtMs());
-
- String ip = s.getClientIp();
- String geo = GeoLookupService.resolveCountryCityOrIpWithCache(ip);
- info.setGeo(geo);
-
- resultList.add(info);
- }
-
- Net_ListSessions_Response resp = new Net_ListSessions_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessions(resultList);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-
-import java.security.SecureRandom;
-import java.sql.SQLException;
-
-/**
- * SessionChallenge (v2) — шаг 1 входа в существующую сессию.
- *
- * Логика авторизации (v2):
- * - Вход в существующую сессию ВСЕГДА в 2 шага:
- * 1) SessionChallenge(sessionId) -> nonce
- * 2) SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
- *
- * Что делает:
- * - Проверяет, что sessionId существует в БД.
- * - Генерирует одноразовый nonce (base64url(32)), сохраняет его в ctx:
- * ctx.sessionLoginNonce, ctx.sessionLoginSessionId, ctx.sessionLoginNonceExpiresAtMs.
- */
-public class Net_SessionChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
- private static final long NONCE_TTL_MS = 60_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionChallenge_Request req = (Net_SessionChallenge_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String nonce = Base64Ws.encode(buf);
-
- long now = System.currentTimeMillis();
- ctx.setSessionLoginNonce(nonce);
- ctx.setSessionLoginSessionId(sessionId);
- ctx.setSessionLoginNonceExpiresAtMs(now + NONCE_TTL_MS);
-
- Net_SessionChallenge_Response resp = new Net_SessionChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setNonce(nonce);
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.SQLException;
-
-/**
- * SessionLogin (v2) — шаг 2 входа в существующую сессию (по sessionKey).
- *
- * Логика авторизации (v2):
- * - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
- * - SessionLogin проверяет подпись sessionKey над строкой:
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- * - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
- *
- * При успехе:
- * - ctx становится AUTH_STATUS_USER
- * - обновляем метаданные сессии (lastAuth + clientIp + clientInfo + lang)
- * - возвращаем storagePwd
- */
-public class Net_SessionLogin_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
-
- private static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- // проверка челленджа
- if (ctx.getSessionLoginNonce() == null
- || ctx.getSessionLoginSessionId() == null
- || System.currentTimeMillis() > ctx.getSessionLoginNonceExpiresAtMs()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_CHALLENGE",
- "Нет активного SessionChallenge или nonce истёк"
- );
- }
-
- if (!sessionId.equals(ctx.getSessionLoginSessionId())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "SESSION_ID_MISMATCH",
- "nonce был выдан для другого sessionId"
- );
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая подпись"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- String sessionPubKeyB64 = session.getSessionKey(); // это pubKey (Base64(32))
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_SESSION_KEY",
- "В сессии не задан session_key"
- );
- }
-
- String nonce = ctx.getSessionLoginNonce();
-
- boolean sigOk;
- try {
- sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный Base64 для ключа/подписи"
- );
- }
-
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- }
-
- // сжигаем nonce
- ctx.setSessionLoginNonce(null);
- ctx.setSessionLoginSessionId(null);
- ctx.setSessionLoginNonceExpiresAtMs(0);
-
- // подтягиваем пользователя
- SolanaUserEntry user;
- try {
- user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_USER_LOOKUP",
- "Ошибка доступа к базе данных при получении пользователя"
- );
- }
-
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "USER_NOT_FOUND_FOR_SESSION",
- "Пользователь для данной сессии не найден"
- );
- }
-
- // обновление метаданных
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String clientIp = null;
- String clientInfoFromRequest = null;
- String userLanguage = null;
-
- if (ctx.getWsSession() != null) {
- clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
- clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
- userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
-
- if (clientIp != null && !clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- long now = System.currentTimeMillis();
- try {
- ActiveSessionsDAO.getInstance().updateOnRefresh(
- sessionId,
- now,
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
- } catch (SQLException e) {
- log.error("Ошибка БД при updateOnRefresh sessionId={}", sessionId, e);
- }
-
- session.setLastAuthirificatedAtMs(now);
- session.setClientIp(clientIp);
- session.setClientInfoFromClient(clientInfoFromClient);
- session.setClientInfoFromRequest(clientInfoFromRequest);
- session.setUserLanguage(userLanguage);
-
- // ctx
- ctx.setActiveSession(session);
- ctx.setSolanaUser(user);
- ctx.setSessionId(sessionId);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // ответ
- Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setStoragePwd(session.getStoragePwd());
- return resp;
- }
-
- private static boolean verifySessionLoginSignature(
- String sessionPubKeyB64,
- String sessionId,
- long timeMs,
- String nonce,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // pubKey: Base64(32). (Ed25519Util.keyFromBase64 должен использовать стандартный Base64)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);
-
- // signature: Base64(64) через единую утилиту WS-протокола
- byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
-
- String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-public final class Net_AddBlock_Request extends Net_Request {
-
- private String blockchainName; // обязателен
- private int blockNumber; // обязателен
- private String prevBlockHash; // HEX(64) или "" для нулевого
- private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public int getBlockNumber() { return blockNumber; }
- public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; }
-
- public String getPrevBlockHash() { return prevBlockHash; }
- public void setPrevBlockHash(String prevBlockHash) { this.prevBlockHash = prevBlockHash; }
-
- public String getBlockBytesB64() { return blockBytesB64; }
- public void setBlockBytesB64(String blockBytesB64) { this.blockBytesB64 = blockBytesB64; }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ:
- * - reasonCode (null если ok)
- * - serverLastGlobalNumber / serverLastGlobalHash
- */
-public final class Net_AddBlock_Response extends Net_Response {
-
- /** null если ok, иначе строка причины (bad_block_base64, user_not_found, и т.п.) */
- private String reasonCode;
-
- /** что сервер считает последним по глобальной цепочке */
- private int serverLastGlobalNumber;
- private String serverLastGlobalHash;
-
- public String getReasonCode() { return reasonCode; }
- public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }
-
- 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; }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain;
-
-import blockchain.BchBlockEntry;
-import blockchain.BchCryptoVerifier;
-import blockchain.MsgSubType;
-import blockchain.body.BodyHasLine;
-import blockchain.body.BodyHasTarget;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ConnectionContext;
-import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainLocks;
-import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
-import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.BlocksDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.BlockEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.util.Arrays;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * Net_AddBlock_Handler — единый хэндлер добавления блока (JSON).
- *
- * Изменение (v3):
- * - ВСЕ ошибки теперь возвращаются в стандартном формате Net_Exception_Response:
- * status != 200, payload: { code, message, serverLastGlobalNumber, serverLastGlobalHash }
- * - Успех — как и раньше Net_AddBlock_Response (status=200).
- */
-public final class Net_AddBlock_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_Handler.class);
-
- private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
- private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) {
-
- Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;
-
- String blockchainName = req.getBlockchainName();
- ReentrantLock lock = BlockchainLocks.lockFor(blockchainName);
- lock.lock();
- try {
- AddBlockResult r = addBlock(
- blockchainName,
- req.getBlockNumber(), // старое поле, пока оставляем
- req.getPrevBlockHash(), // старое поле, пока оставляем
- req.getBlockBytesB64()
- );
-
- // ✅ УСПЕХ: как раньше
- if (r.isOk()) {
- Net_AddBlock_Response resp = new Net_AddBlock_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setReasonCode(null);
- resp.setServerLastGlobalNumber(r.serverLastBlockNumber);
- resp.setServerLastGlobalHash(r.serverLastBlockHashHex);
-
- return resp;
- }
-
- // ✅ ОШИБКА: стандартный формат (code + message) + доп.поля для ресинка
- return error(req, r.httpStatus, r.reasonCode, r.serverLastBlockNumber, r.serverLastBlockHashHex);
-
- } finally {
- lock.unlock();
- }
- }
-
- private Net_Response error(Net_AddBlock_Request req,
- int status,
- String reasonCode,
- int serverLastNum,
- String serverLastHashHex) {
-
- AddBlockExceptionResponse resp = new AddBlockExceptionResponse();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(status);
-
- // code — машинный
- resp.setCode(reasonCode != null ? reasonCode : "add_block_error");
- // message — человеческий (можешь улучшать тексты как угодно)
- resp.setMessage(humanMessage(reasonCode));
-
- // полезно клиенту для ресинка
- resp.setServerLastGlobalNumber(serverLastNum);
- resp.setServerLastGlobalHash(serverLastHashHex);
-
- return resp;
- }
-
- private static String humanMessage(String code) {
- if (code == null) return "Ошибка добавления блока";
-
- return switch (code) {
- case "empty_blockchain_name" -> "Пустое имя блокчейна";
- case "bad_blockchain_name" -> "Некорректное имя блокчейна";
- case "db_error" -> "Ошибка базы данных";
- case "blockchain_state_not_found" -> "Состояние блокчейна не найдено";
- case "state_last_hash_invalid" -> "Повреждено состояние блокчейна: неверный last_block_hash";
- case "bad_block_base64" -> "Некорректный base64 блока";
- case "limit_exceeded" -> "Превышен лимит размера блокчейна";
- case "limit_check_failed" -> "Ошибка проверки лимита размера";
- case "bad_block_format" -> "Некорректный формат блока";
- case "bad_block_body" -> "Некорректное тело блока";
- case "bad_block_number" -> "Некорректный номер блока";
- case "req_global_mismatch" -> "Номер блока в запросе не совпадает с номером в блоке";
- case "bad_prev_hash" -> "Некорректный prevHash (цепочка не совпадает)";
- case "bad_blockchain_key_len" -> "Некорректный ключ блокчейна в состоянии (ожидалось 32 байта)";
- case "signature_verify_failed" -> "Ошибка проверки подписи блока";
- case "bad_signature" -> "Некорректная подпись блока";
- case "prev_line_block_not_found" -> "Не найден блок prevLineNumber для проверки линии";
- case "bad_prev_line_hash" -> "Некорректный prevLineHash";
- case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
- case "internal_error" -> "Внутренняя ошибка сервера при записи блока";
- default -> "Ошибка: " + code;
- };
- }
-
- private AddBlockResult addBlock(
- String blockchainName,
- int globalNumberFromReq,
- String prevGlobalHashHexFromReq,
- String blockBytesB64
- ) {
- if (blockchainName == null || blockchainName.isBlank()) {
- log.warn("AddBlock: пустой blockchainName (reqGlobalNumber={})", globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
- }
-
- String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
- if (login == null || login.isBlank()) {
- log.warn("AddBlock: плохой blockchainName='{}' => login не получился (reqGlobalNumber={})",
- blockchainName, globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
- }
-
- // 1) state обязателен
- final BlockchainStateEntry st;
- try {
- st = stateDAO.getByBlockchainName(blockchainName);
- } catch (Exception e) {
- log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
- }
-
- if (st == null) {
- log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq);
- return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
- }
-
- final int serverLastNum = st.getLastBlockNumber();
-
- final byte[] serverLastHash32;
- try {
- serverLastHash32 = (serverLastNum < 0)
- ? new byte[32]
- : require32OrThrow(st.getLastBlockHash(), "state.last_block_hash is null/invalid");
- } catch (Exception e) {
- // ✅ Раньше тут мог вылететь неожиданный 500 через внешний try/catch.
- log.error("AddBlock: state_last_hash_invalid (login={}, blockchainName={}, serverLastNum={})",
- login, blockchainName, serverLastNum, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "state_last_hash_invalid", serverLastNum, "");
- }
-
- final String serverLastHashHex = toHex(serverLastHash32);
-
- // 2) decode block
- final byte[] blockBytes;
- try {
- blockBytes = decodeBase64(blockBytesB64);
- } catch (Exception e) {
- log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, reqGlobalNumber={})",
- login, blockchainName, globalNumberFromReq, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
- }
-
- // 3) лимит (оставляем как было)
- try {
- long oldSize = st.getFileSizeBytes();
- long limit = st.getSizeLimit();
- long newSize = safeAdd(oldSize, blockBytes.length);
-
- if (limit > 0 && newSize > limit) {
- log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, oldSize={}, addLen={}, newSize={}, limit={})",
- login, blockchainName, oldSize, blockBytes.length, newSize, limit);
- return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
- }
- } catch (Exception e) {
- log.error("AddBlock: limit_check_failed (login={}, blockchainName={})", login, blockchainName, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
- }
-
- // 4) parse block
- final BchBlockEntry block;
- try {
- block = new BchBlockEntry(blockBytes);
- } catch (Exception e) {
- log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, bytesLen={})",
- login, blockchainName, blockBytes.length, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
- }
-
- // body.check()
- try {
- block.body.check();
- } catch (Exception e) {
- log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, blockNumber={}, type={}, ver={})",
- login, blockchainName, block.blockNumber, (block.type & 0xFFFF), (block.version & 0xFFFF), e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
- }
-
- // 4.2) запрет дырок: blockNumber строго last+1
- int expectedBlockNumber = serverLastNum + 1;
- if (block.blockNumber != expectedBlockNumber) {
- log.warn("AddBlock: bad_block_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={})",
- login, blockchainName, block.blockNumber, expectedBlockNumber, serverLastNum);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_number", serverLastNum, serverLastHashHex);
- }
-
- // (временная совместимость) req.globalNumber должен совпасть с block.blockNumber
- if (globalNumberFromReq != block.blockNumber) {
- log.warn("AddBlock: req_global_mismatch (login={}, blockchainName={}, reqGlobal={}, blockNumber={})",
- login, blockchainName, globalNumberFromReq, block.blockNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "req_global_mismatch", serverLastNum, serverLastHashHex);
- }
-
- // 4.3) проверка цепочки по prevHash32
- if (!Arrays.equals(block.prevHash32, serverLastHash32)) {
- log.warn("AddBlock: bad_prev_hash (login={}, blockchainName={}, blockNumber={}, clientPrev={}, serverPrev={})",
- login, blockchainName, block.blockNumber, toHex(block.prevHash32), serverLastHashHex);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_hash", serverLastNum, serverLastHashHex);
- }
-
- // 5) pubKey
- final byte[] pubKey32 = st.getBlockchainKeyBytes();
- if (pubKey32 == null || pubKey32.length != 32) {
- log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, blockNumber={}, keyLen={})",
- login, blockchainName, block.blockNumber, (pubKey32 == null ? -1 : pubKey32.length));
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
- }
-
- // 6) подпись по hash32(preimage)
- boolean sigOk;
- try {
- sigOk = BchCryptoVerifier.verifyBlock(block, pubKey32);
- } catch (Exception e) {
- log.warn("AddBlock: signature_verify_failed (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "signature_verify_failed", serverLastNum, serverLastHashHex);
- }
-
- if (!sigOk) {
- log.warn("AddBlock: bad_signature (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
- }
-
- // 7) line columns (only for BodyHasLine)
- Integer lineCode = null;
- Integer prevLineNumber = null;
- byte[] prevLineHash32 = null;
- Integer thisLineNumber = null;
-
- if (block.body instanceof BodyHasLine bl) {
- lineCode = bl.lineCode();
- prevLineNumber = bl.prevLineBlockGlobalNumber();
- prevLineHash32 = bl.prevLineBlockHash32();
- thisLineNumber = bl.lineSeq();
-
- // Нормализация: -1 не пишем в БД (для совместимости со старым TextBody)
- if (prevLineNumber != null && prevLineNumber == -1) {
- prevLineNumber = null;
- prevLineHash32 = null;
- thisLineNumber = null;
- }
-
- // Если prevLineNumber задан — проверяем его хэш
- if (prevLineNumber != null) {
- try {
- byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
- if (dbPrevHash == null) {
- log.warn("AddBlock: prev_line_block_not_found (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
- login, blockchainName, block.blockNumber, prevLineNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_line_block_not_found", serverLastNum, serverLastHashHex);
- }
- if (!Arrays.equals(dbPrevHash, require32OrThrow(prevLineHash32, "prevLineHash32 invalid"))) {
- log.warn("AddBlock: bad_prev_line_hash (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
- login, blockchainName, block.blockNumber, prevLineNumber);
- return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_line_hash", serverLastNum, serverLastHashHex);
- }
- } catch (Exception e) {
- log.error("AddBlock: db_error_prev_line_check (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_prev_line_check", serverLastNum, serverLastHashHex);
- }
- }
- }
-
- // 8) сформировать запись и записать (DB + state + файл)
- try {
- BlockEntry be = new BlockEntry();
- be.setLogin(login);
- be.setBchName(blockchainName);
-
- be.setBlockNumber(block.blockNumber);
- be.setMsgType(block.type & 0xFFFF);
- be.setMsgSubType(block.subType & 0xFFFF);
-
- be.setBlockBytes(block.toBytes());
- be.setBlockHash(block.getHash32());
- be.setBlockSignature(block.getSignature64());
-
- // line columns (optional)
- be.setLineCode(lineCode);
- be.setPrevLineNumber(prevLineNumber);
- be.setPrevLineHash(prevLineHash32);
- be.setThisLineNumber(thisLineNumber);
-
- // target columns (optional)
- if (block.body instanceof BodyHasTarget t) {
- be.setToLogin(t.toLogin());
- be.setToBchName(t.toBchName());
- be.setToBlockNumber(t.toBlockGlobalNumber());
- be.setToBlockHash(t.toBlockHashBytes());
- }
-
- // edit helper (optional): если TEXT_EDIT_* — это "редактирование блока цели"
- int type = block.type & 0xFFFF;
- int sub = block.subType & 0xFFFF;
-
- if (type == 1
- && (sub == (MsgSubType.TEXT_EDIT_POST & 0xFFFF) || sub == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF))
- && be.getToBlockNumber() != null) {
- be.setEditedByBlockNumber(be.getToBlockNumber());
- }
-
- dbWriter.appendBlockAndState(blockchainName, block, st, be);
-
- } catch (Exception e) {
- log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",
- login, blockchainName, block.blockNumber, e);
- return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
- }
-
- String newHashHex = toHex(block.getHash32());
-
- log.info("✅ AddBlock ok: login={}, blockchainName={}, blockNumber={}, newHash={}",
- login, blockchainName, block.blockNumber, newHashHex);
-
- return new AddBlockResult(WireCodes.Status.OK, null, block.blockNumber, newHashHex);
- }
-
- /* ===================================================================== */
- /* ====================== Helpers ====================================== */
- /* ===================================================================== */
-
- private static byte[] decodeBase64(String b64) {
- if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
- return Base64Ws.decode(b64);
- }
-
- private static long safeAdd(long a, long b) {
- long r = a + b;
- if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
- return r;
- }
-
- private static byte[] require32OrThrow(byte[] b, String msg) {
- if (b == null || b.length != 32) throw new IllegalArgumentException(msg);
- return b;
- }
-
- private static String toHex(byte[] bytes) {
- if (bytes == null) return "null";
- char[] HEX = "0123456789abcdef".toCharArray();
- char[] out = new char[bytes.length * 2];
- for (int i = 0; i < bytes.length; i++) {
- int v = bytes[i] & 0xFF;
- out[i * 2] = HEX[v >>> 4];
- out[i * 2 + 1] = HEX[v & 0x0F];
- }
- return new String(out);
- }
-
- /**
- * Спец-ответ ошибки AddBlock: стандартный code/message + поля для ресинка.
- * В wire-формате это окажется внутри payload.
- */
- public static final class AddBlockExceptionResponse extends Net_Exception_Response {
- private Integer serverLastGlobalNumber;
- private String serverLastGlobalHash;
-
- public Integer getServerLastGlobalNumber() {
- return serverLastGlobalNumber;
- }
-
- public void setServerLastGlobalNumber(Integer serverLastGlobalNumber) {
- this.serverLastGlobalNumber = serverLastGlobalNumber;
- }
-
- public String getServerLastGlobalHash() {
- return serverLastGlobalHash;
- }
-
- public void setServerLastGlobalHash(String serverLastGlobalHash) {
- this.serverLastGlobalHash = serverLastGlobalHash;
- }
- }
-
- private static final class AddBlockResult {
- final int httpStatus;
- final String reasonCode;
- final int serverLastBlockNumber;
- final String serverLastBlockHashHex;
-
- AddBlockResult(int httpStatus, String reasonCode, int serverLastBlockNumber, String serverLastBlockHashHex) {
- this.httpStatus = httpStatus;
- this.reasonCode = reasonCode;
- this.serverLastBlockNumber = serverLastBlockNumber;
- this.serverLastBlockHashHex = serverLastBlockHashHex;
- }
-
- boolean isOk() { return httpStatus == WireCodes.Status.OK; }
- }
-}
-
-package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.ReentrantLock;
-
-public final class BlockchainLocks {
- private static final ConcurrentHashMap MAP = new ConcurrentHashMap<>();
-
- private BlockchainLocks() {}
-
- public static ReentrantLock lockFor(String blockchainName) {
- return MAP.computeIfAbsent(blockchainName, id -> new ReentrantLock(true)); // fair=true
- }
-}
-package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;
-
-import blockchain.BchBlockEntry;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.BlocksDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.BlockEntry;
-import utils.files.FileStoreUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-/**
- * BlockchainWriter — запись блока в DB + обновление state + запись в файл.
- *
- * ВАЖНО:
- * - Это минимальный рабочий вариант под новый формат.
- * - Если у тебя уже есть "атомарность" сложнее (tmp_bch + commit/recovery) — можно усилить потом.
- */
-public final class BlockchainWriter {
-
- private final BlocksDAO blocksDAO;
- private final BlockchainStateDAO stateDAO;
- private final FileStoreUtil fs = FileStoreUtil.getInstance();
-
- public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
- this.blocksDAO = blocksDAO;
- this.stateDAO = stateDAO;
- }
-
- public void appendBlockAndState(String blockchainName,
- BchBlockEntry block,
- BlockchainStateEntry st,
- BlockEntry be) throws SQLException {
-
- long nowMs = System.currentTimeMillis();
-
- try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
- c.setAutoCommit(false);
- try {
- // 1) insert block
- blocksDAO.insert(c, be);
-
- // 2) update state
- st.setLastBlockNumber(block.blockNumber);
- st.setLastBlockHash(block.getHash32());
- st.setFileSizeBytes(st.getFileSizeBytes() + block.toBytes().length);
- st.setUpdatedAtMs(nowMs);
-
- stateDAO.upsert(c, st);
-
- c.commit();
- } catch (Exception e) {
- try { c.rollback(); } catch (Exception ignored) {}
- if (e instanceof SQLException se) throw se;
- throw new SQLException("appendBlockAndState failed", e);
- } finally {
- try { c.setAutoCommit(true); } catch (Exception ignored) {}
- }
- }
-
- // 3) append to file (минимально: просто дописать)
- // Если у тебя уже есть логика tmp_bch+atomicReplace — можно заменить тут.
- String fileName = fs.buildBlockchainFileName(blockchainName);
- fs.addDataToFile(fileName, block.toBytes());
- }
-}
-package server.logic.ws_protocol.JSON.handlers.connections.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetFriendsLists — получить два списка "друзей" по connections_state.
- *
- * {
- * "op": "GetFriendsLists",
- * "requestId": "req-100",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * Возвращает:
- * - out_friends: кому login поставил FRIEND
- * - in_friends: кто поставил FRIEND этому login
- *
- * ПРО ДОСТУП (на будущее):
- * Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей.
- */
-public class Net_GetFriendsLists_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.connections.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ GetFriendsLists.
- *
- * {
- * "op": "GetFriendsLists",
- * "requestId": "req-100",
- * "status": 200,
- * "payload": {
- * "login": "Anya", // канонический регистр из БД
- * "out_friends": ["Bob", "Kate"], // кому login поставил FRIEND
- * "in_friends": ["Alex", "Kate"] // кто поставил FRIEND login
- * }
- * }
- */
-public class Net_GetFriendsLists_Response extends Net_Response {
-
- private String login;
-
- private List out_friends = new ArrayList<>();
- private List in_friends = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List getOut_friends() { return out_friends; }
- public void setOut_friends(List out_friends) { this.out_friends = out_friends; }
-
- public List getIn_friends() { return in_friends; }
- public void setIn_friends(List in_friends) { this.in_friends = in_friends; }
-}
-package server.logic.ws_protocol.JSON.handlers.connections;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.MsgSubType;
-import shine.db.SqliteDbController;
-import shine.db.dao.ConnectionsStateDAO;
-
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.util.List;
-
-/**
- * GetFriendsLists — получить 2 списка:
- * - out_friends: кому login поставил FRIEND
- * - in_friends: кто поставил FRIEND этому login
- *
- * ВАЖНО:
- * - login в запросе может быть любым регистром
- * - в ответе возвращаем канонический регистр (как в solana_users.login)
- *
- * ПРИМЕЧАНИЕ:
- * Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
- */
-public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- final String loginAnyCase = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
-
- // 1) Канонизируем login через solana_users (NOCASE)
- String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
- if (canonicalLogin == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- int relType = (int) MsgSubType.CONNECTION_FRIEND;
-
- // 2) Два списка (логины канонические)
- List outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType);
- List inFriends = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType);
-
- Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(canonicalLogin);
- resp.setOut_friends(outFriends);
- resp.setIn_friends(inFriends);
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetFriendsLists", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-
- private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
- String sql = """
- SELECT login
- FROM solana_users
- WHERE login = ? COLLATE NOCASE
- LIMIT 1
- """;
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, loginAnyCase);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return rs.getString("login");
- }
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers;
-
-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;
-
-/**
- * Общий интерфейс для всех JSON-хэндлеров.
- */
-public interface JsonMessageHandler {
-
- /**
- * Обработать запрос и вернуть ответ.
- *
- * @param request распарсенный запрос
- * @param ctx контекст текущего WebSocket-соединения
- */
- Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception;
-}
-
-package server.logic.ws_protocol.JSON.handlers.system.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Ping:
- * {
- * "op": "Ping",
- * "requestId": "req-1",
- * "payload": { "ts": 1700000000000 }
- * }
- *
- * Сервер ничего не проверяет, поле ts можно слать любое.
- */
-public class Net_Ping_Request extends Net_Request {
-
- private long ts;
-
- public long getTs() { return ts; }
- public void setTs(long ts) { this.ts = ts; }
-}
-package server.logic.ws_protocol.JSON.handlers.system.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Pong-ответ:
- * {
- * "op": "Ping",
- * "requestId": "req-1",
- * "status": 200,
- * "payload": { "ts": 1700000000123 }
- * }
- */
-public class Net_Ping_Response extends Net_Response {
-
- private long ts;
-
- public long getTs() { return ts; }
- public void setTs(long ts) { this.ts = ts; }
-}
-package server.logic.ws_protocol.JSON.handlers.system;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
-import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Response;
-import server.logic.ws_protocol.WireCodes;
-
-/**
- * Ping — keep-alive.
- * В ответ кладём только ts (текущее время сервера в мс).
- */
-public class Net_Ping_Handler implements JsonMessageHandler {
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_Ping_Request req = (Net_Ping_Request) baseRequest;
-
- Net_Ping_Response resp = new Net_Ping_Response();
- resp.setOp(req.getOp()); // "Ping"
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- // ничего не проверяем, просто отдаём серверное время
- resp.setTs(System.currentTimeMillis());
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос AddUser — временная/тестовая регистрация локального пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "payload": {
- * "login": "anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "base64-ed25519-public-key-login",
- * "blockchainKey": "base64-ed25519-public-key-blockchain",
- * "deviceKey": "base64-ed25519-public-key-device",
- * "bchLimit": 1000000
- * }
- * }
- *
- * Все поля лежат внутри payload.
- */
-public class Net_AddUser_Request extends Net_Request {
-
- private String login;
- private String blockchainName;
-
- /** Ключ пользователя Solana (публичный ключ логина) */
- private String solanaKey;
-
- /** Ключ блокчейна (публичный ключ блокчейна) */
- private String blockchainKey;
-
- /** Ключ устройства (публичный ключ устройства) */
- private String deviceKey;
-
- private Integer bchLimit;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-
- public Integer getBchLimit() { return bchLimit; }
- public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
-}
-// file: server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Успешный ответ на AddUser.
- *
- * Сейчас дополнительных полей нет — достаточно status=200.
- *
- * Пример:
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_AddUser_Response extends Net_Response {
- // При необходимости сюда можно добавить, например, флаг created/updated и т.п.
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUser — проверка/получение пользователя по login.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "payload": {
- * "login": "AnYa"
- * }
- * }
- *
- * Поиск по login выполняется без учёта регистра.
- * В ответе возвращаем login/blockchainName с тем регистром, как в БД.
- */
-public class Net_GetUser_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUser.
- *
- * Всегда status=200.
- *
- * Пример (нет пользователя):
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "status": 200,
- * "payload": { "exists": false }
- * }
- *
- * Пример (есть пользователь):
- * {
- * "op": "GetUser",
- * "requestId": "u-1",
- * "status": 200,
- * "payload": {
- * "exists": true,
- * "login": "Anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "...",
- * "blockchainKey": "...",
- * "deviceKey": "..."
- * }
- * }
- */
-public class Net_GetUser_Response extends Net_Response {
-
- private Boolean exists;
-
- private String login;
- private String blockchainName;
- private String solanaKey;
- private String blockchainKey;
- private String deviceKey;
-
- public Boolean getExists() { return exists; }
- public void setExists(Boolean exists) { this.exists = exists; }
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос SearchUsers — поиск логинов по префиксу.
- *
- * Клиент отправляет:
- * {
- * "op": "SearchUsers",
- * "requestId": "su-1",
- * "payload": { "prefix": "any" }
- * }
- *
- * Поиск по prefix выполняется без учёта регистра.
- * В ответе возвращаем логины с тем регистром, как в БД.
- */
-public class Net_SearchUsers_Request extends Net_Request {
-
- private String prefix;
-
- public String getPrefix() { return prefix; }
- public void setPrefix(String prefix) { this.prefix = prefix; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ SearchUsers.
- *
- * Всегда status=200.
- *
- * Пример:
- * {
- * "op": "SearchUsers",
- * "requestId": "su-1",
- * "status": 200,
- * "payload": {
- * "logins": ["Anya", "andrew", "Angel"]
- * }
- * }
- */
-public class Net_SearchUsers_Response extends Net_Response {
-
- private List logins = new ArrayList<>();
-
- public List getLogins() { return logins; }
- public void setLogins(List logins) { this.logins = logins; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.SolanaUserEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-public class Net_AddUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
-
- /** TEST ONLY */
- private static final int TEST_BCH_LIMIT = 1_000_000;
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getBlockchainName() == null || req.getBlockchainName().isBlank()
- || req.getSolanaKey() == null || req.getSolanaKey().isBlank()
- || req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
- || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
- );
- }
-
- // blockchainName должен быть вида: -NNN
- if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BLOCKCHAIN_NAME",
- "blockchainName должен быть вида -NNN (пример: anya-001)"
- );
- }
-
- int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0)
- ? TEST_BCH_LIMIT
- : req.getBchLimit();
-
- try {
- // базовая валидация форматов ключей: Base64(32 bytes)
- byte[] solanaKey32;
- byte[] blockchainKey32;
- byte[] deviceKey32;
-
- try {
- solanaKey32 = Base64Ws.decodeLen(req.getSolanaKey(), 32, "solanaKey");
- blockchainKey32 = Base64Ws.decodeLen(req.getBlockchainKey(), 32, "blockchainKey");
- deviceKey32 = Base64Ws.decodeLen(req.getDeviceKey(), 32, "deviceKey");
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_KEY_FORMAT",
- e.getMessage()
- );
- }
-
- // (переменные не используются дальше, но оставляем для ясности проверки длины)
- if (solanaKey32.length != 32 || blockchainKey32.length != 32 || deviceKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_KEY_FORMAT",
- "solanaKey/blockchainKey/deviceKey должны быть Base64(32 bytes)"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- SqliteDbController db = SqliteDbController.getInstance();
-
- try (Connection c = db.getConnection()) {
- c.setAutoCommit(false);
-
- // 1. Проверяем, что пользователя нет (case-insensitive)
- if (usersDAO.getByLogin(c, req.getLogin()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "USER_ALREADY_EXISTS",
- "Пользователь с таким login уже существует"
- );
- }
-
- // 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД)
- if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_ALREADY_EXISTS",
- "Пользователь с таким blockchainName уже существует"
- );
- }
-
- // 3. На всякий случай оставляем старую проверку blockchain_state,
- // потому что эта таблица нужна серверу (состояние цепочки/лимиты).
- if (stateDAO.getByBlockchainName(c, req.getBlockchainName()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_STATE_ALREADY_EXISTS",
- "blockchain_state уже существует"
- );
- }
-
- // 4. Создаём пользователя (все поля теперь лежат в solana_users)
- SolanaUserEntry user = new SolanaUserEntry();
- user.setLogin(req.getLogin());
- user.setBlockchainName(req.getBlockchainName());
- user.setSolanaKey(req.getSolanaKey());
- user.setBlockchainKey(req.getBlockchainKey());
- user.setDeviceKey(req.getDeviceKey());
-
- usersDAO.insert(c, user);
-
- // 5. Создаём INITIAL blockchain_state (для работы сервера)
- BlockchainStateEntry st = new BlockchainStateEntry();
- st.setBlockchainName(req.getBlockchainName());
- st.setLogin(req.getLogin());
- st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
- st.setLastBlockNumber(-1);
- st.setLastBlockHash(new byte[32]);
- st.setFileSizeBytes(0);
- st.setSizeLimit(limit);
- st.setUpdatedAtMs(System.currentTimeMillis());
-
- stateDAO.upsert(c, st);
-
- c.commit();
- }
-
- Net_AddUser_Response resp = new Net_AddUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
- req.getLogin(), req.getBlockchainName(), limit);
-
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-public class Net_GetUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUser_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUser_Request req = (Net_GetUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- // тут логичнее BAD_REQUEST, но ты просил: "нет пользователя" тоже 200.
- // Поэтому BAD_REQUEST оставляем только на реально пустой login.
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
-
- try {
- SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
-
- Net_GetUser_Response resp = new Net_GetUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (u == null) {
- resp.setExists(false);
- log.info("ℹ️ GetUser: not found for login={}", req.getLogin());
- return resp;
- }
-
- // ВАЖНО:
- // - Поиск по login был case-insensitive,
- // - а тут возвращаем login/blockchainName как в БД (с исходным регистром).
- resp.setExists(true);
- resp.setLogin(u.getLogin());
- resp.setBlockchainName(u.getBlockchainName());
- resp.setSolanaKey(u.getSolanaKey());
- resp.setBlockchainKey(u.getBlockchainKey());
- resp.setDeviceKey(u.getDeviceKey());
-
- log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName());
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error GetUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error GetUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class Net_SearchUsers_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SearchUsers_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_SearchUsers_Request req = (Net_SearchUsers_Request) baseRequest;
-
- if (req.getPrefix() == null || req.getPrefix().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: prefix"
- );
- }
-
- String prefix = req.getPrefix().trim();
-
- try {
- SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
- List users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5
-
- List logins = new ArrayList<>();
- for (SolanaUserEntry u : users) {
- if (u != null && u.getLogin() != null) {
- logins.add(u.getLogin()); // регистр как в БД
- }
- }
-
- Net_SearchUsers_Response resp = new Net_SearchUsers_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setLogins(logins);
-
- log.info("✅ SearchUsers ok: prefix='{}' -> {}", prefix, logins.size());
- return resp;
-
- } catch (SQLException e) {
- log.error("❌ DB error SearchUsers", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error SearchUsers", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUserParam — получить один параметр пользователя.
- *
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) этот запрос не ограничивает просмотр параметров, т.к. проект в тестовом режиме.
- * Позже, вероятно, потребуется ограничить: кто и какие параметры может читать (сессия/права).
- * Но для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUserParam.
- *
- * Если найден:
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * }
- * }
- *
- * Если не найден:
- * status=404, payload пустой.
- */
-public class Net_GetUserParam_Response extends Net_Response {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListUserParams — получить все сохранённые параметры пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ ListUserParams — список всех параметров пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "params": [
- * {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * },
- * ...
- * ]
- * }
- * }
- */
-public class Net_ListUserParams_Response extends Net_Response {
-
- private String login;
- private List
- params = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List
- getParams() { return params; }
- public void setParams(List
- params) { this.params = params; }
-
- public static class Item {
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-ed25519-public-key-32",
- * "signature": "base64-ed25519-signature-64"
- * }
- * }
- *
- * Подпись считается от UTF-8 строки:
- * USER_PARAMETER_PREFIX + login + param + time_ms + value
- */
-public class Net_UpsertUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
-
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на UpsertUserParam.
- *
- * Успех:
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_UpsertUserParam_Response extends Net_Response {
- // MVP: без payload. При желании позже можно добавить created/updated.
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-
-/**
- * GetUserParam — получить один параметр пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUserParam_Request req = (Net_GetUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param"
- );
- }
-
- String login = req.getLogin().trim();
- String param = req.getParam().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- UserParamEntry e = dao.getByLoginAndParam(c, login, param);
-
- if (e == null) {
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(404);
- return resp;
- }
-
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(e.getLogin());
- resp.setParam(e.getParam());
- resp.setTime_ms(e.getTimeMs());
- resp.setValue(e.getValue());
- resp.setDevice_key(e.getDeviceKey());
- resp.setSignature(e.getSignature());
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListUserParams — получить все параметры пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListUserParams_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_ListUserParams_Request req = (Net_ListUserParams_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- String login = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- List entries;
- try (Connection c = db.getConnection()) {
- entries = dao.getByLogin(c, login);
- }
-
- Net_ListUserParams_Response resp = new Net_ListUserParams_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(login);
-
- List items = new ArrayList<>();
- for (UserParamEntry e : entries) {
- Net_ListUserParams_Response.Item it = new Net_ListUserParams_Response.Item();
- it.setLogin(e.getLogin());
- it.setParam(e.getParam());
- it.setTime_ms(e.getTimeMs());
- it.setValue(e.getValue());
- it.setDevice_key(e.getDeviceKey());
- it.setSignature(e.getSignature());
- items.add(it);
- }
- resp.setParams(items);
-
- return resp;
-
- } catch (Exception e) {
- log.error("❌ Internal error ListUserParams", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.SolanaUserEntry;
-import shine.db.entities.UserParamEntry;
-import utils.config.ShineSignatureConstants;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.Connection;
-import java.sql.SQLException;
-
-/**
- * Net_UpsertUserParam_Handler
- *
- * Делает (MVP, без "сессий"):
- * 1) Проверка входных полей.
- * 2) Проверка подписи Ed25519 по device_key.
- * 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
- * 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
- *
- * ВАЖНО:
- * - НИКАКИХ ручных транзакций / BEGIN здесь нет.
- * - autoCommit=true, каждый statement завершённый сам по себе.
- * - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
- * наш финальный UPSERT просто вернёт 0 обновлённых строк.
- */
-public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()
- || req.getTime_ms() == null || req.getTime_ms() <= 0
- || req.getValue() == null
- || req.getDevice_key() == null || req.getDevice_key().isBlank()
- || req.getSignature() == null || req.getSignature().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param/time_ms/value/device_key/signature"
- );
- }
-
- final String login = req.getLogin().trim();
- final String param = req.getParam().trim();
- final long timeMs = req.getTime_ms();
- final String value = req.getValue();
- final String deviceKeyB64 = req.getDevice_key().trim();
- final String signatureB64 = req.getSignature().trim();
-
- try {
- // ---------------- Base64 decode ----------------
- byte[] pubKey32;
- byte[] sig64;
- try {
- pubKey32 = Base64Ws.decodeLen(deviceKeyB64, 32, "device_key");
- sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "device_key/signature должны быть Base64"
- );
- }
-
- // ---------------- Signature verify ----------------
- String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
- + login
- + param
- + timeMs
- + value;
-
- byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
-
- boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "SIGNATURE_INVALID",
- "Подпись не прошла проверку"
- );
- }
-
- // ---------------- DB checks + upsert ----------------
- SqliteDbController db = SqliteDbController.getInstance();
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- // 1) user exists
- SolanaUserEntry user = usersDAO.getByLogin(c, login);
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- // 2) device key must match the user's stored deviceKey
- String userDeviceKey = user.getDeviceKey();
- if (userDeviceKey == null || userDeviceKey.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "USER_DEVICE_KEY_EMPTY",
- "У пользователя не задан deviceKey в БД"
- );
- }
-
- if (!userDeviceKey.trim().equals(deviceKeyB64)) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "DEVICE_KEY_MISMATCH",
- "device_key не соответствует пользователю"
- );
- }
-
- // 3) atomic upsert-if-newer
- UserParamEntry e = new UserParamEntry(
- login,
- param,
- timeMs,
- value,
- deviceKeyB64,
- signatureB64
- );
-
- int changed = paramsDAO.upsertIfNewer(c, e);
-
- Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (changed == 1) {
- log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
- } else {
- // 0 строк — значит в БД уже есть time_ms >= incoming
- log.info("ℹ️ UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
- }
-
- return resp;
- }
-
- } catch (SQLException e) {
- log.error("❌ DB error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java
index 1aca1a9..3b956b8 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java
@@ -20,9 +20,9 @@ import java.security.SecureRandom;
* AuthChallenge (v2) — шаг 1 создания новой сессии.
*
* Логика авторизации (v2):
- * - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
+ * - Создание новой сессии возможно ТОЛЬКО через clientKey пользователя.
* - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
- * CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
+ * CreateAuthSession(..., signature(clientKey, AUTH_CREATE_SESSION:...))
*
* Что делает:
* 1) Проверяет login.
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java
index be89190..4086c0e 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java
@@ -30,7 +30,7 @@ import java.security.SecureRandom;
import java.sql.SQLException;
/**
- * CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
+ * CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО clientKey).
*
* Логика авторизации (v2):
* - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
@@ -38,7 +38,7 @@ import java.sql.SQLException;
* отправляет на сервер sessionKey целиком одной строкой.
* - Сервер сохраняет sessionKey в active_sessions.session_key как есть.
*
- * Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
+ * Подпись clientKey (Ed25519) проверяется над строкой (UTF-8):
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
*
* На выходе:
@@ -226,15 +226,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
}
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getClientPlatform());
- String deviceKeyFromDb = user.getDeviceKey();
- if (deviceKeyFromDb == null || deviceKeyFromDb.isBlank()) {
+ String clientKeyFromDb = user.getClientKey();
+ if (clientKeyFromDb == null || clientKeyFromDb.isBlank()) {
Net_Response err = NetExceptionResponseFactory.error(
req,
WireCodes.Status.BAD_REQUEST,
"NO_DEVICE_KEY",
- "Отсутствует deviceKey у пользователя"
+ "Отсутствует clientKey у пользователя"
);
- closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: no deviceKey");
+ closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: no clientKey");
return err;
}
@@ -261,28 +261,28 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
return err;
}
- String deviceKeyFromReq = req.getDeviceKey();
- if (deviceKeyFromReq == null || deviceKeyFromReq.isBlank()) {
+ String clientKeyFromReq = req.getClientKey();
+ if (clientKeyFromReq == null || clientKeyFromReq.isBlank()) {
Net_Response err = NetExceptionResponseFactory.error(
req,
WireCodes.Status.BAD_REQUEST,
"EMPTY_DEVICE_KEY",
- "Пустой deviceKey"
+ "Пустой clientKey"
);
- closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: empty deviceKey");
+ closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: empty clientKey");
return err;
}
- deviceKeyFromReq = deviceKeyFromReq.trim();
+ clientKeyFromReq = clientKeyFromReq.trim();
- // TODO: для ротации device_key стоит дополнительно сверять актуальное значение через Solana.
- if (!deviceKeyFromReq.equals(deviceKeyFromDb)) {
+ // TODO: для ротации client_key стоит дополнительно сверять актуальное значение через Solana.
+ if (!clientKeyFromReq.equals(clientKeyFromDb)) {
Net_Response err = NetExceptionResponseFactory.error(
req,
WireCodes.Status.UNVERIFIED,
"DEVICE_KEY_NOT_ACTUAL",
- "device_key не соответствует актуальной версии"
+ "client_key не соответствует актуальной версии"
);
- closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: device key mismatch");
+ closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: client key mismatch");
return err;
}
@@ -294,7 +294,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
storagePwd,
authNonce,
timeMs,
- deviceKeyFromDb,
+ clientKeyFromDb,
signatureB64
);
} catch (UnsupportedOperationException ex) {
@@ -302,9 +302,9 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
req,
422,
"UNSUPPORTED_KEY_ALGORITHM",
- "deviceKey algorithm is not supported"
+ "clientKey algorithm is not supported"
);
- closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: unsupported device key algorithm");
+ closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: unsupported client key algorithm");
return err;
} catch (IllegalArgumentException ex) {
Net_Response err = NetExceptionResponseFactory.error(
@@ -440,11 +440,11 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
String storagePwd,
String authNonce,
long timeMs,
- String deviceKey,
+ String clientKey,
String signatureB64
) throws IllegalArgumentException {
- byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(deviceKey, "deviceKey");
+ byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(clientKey, "clientKey");
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
String preimageStr = "AUTH_CREATE_SESSION:"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java
index 6b76347..9ae3e6e 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java
@@ -48,9 +48,9 @@ public final class SolanaUserPdaImportService {
boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain(
parsed.login,
parsed.blockchainName,
- parsed.deviceKeyB64, // в текущей модели solanaKey = deviceKey
+ parsed.clientKeyB64, // в текущей модели solanaKey = clientKey
parsed.blockchainKeyB64,
- parsed.deviceKeyB64,
+ parsed.clientKeyB64,
sizeLimit,
now
);
@@ -158,7 +158,7 @@ public final class SolanaUserPdaImportService {
int blocksCount = u8(raw, c++);
String blockchainName = null;
byte[] blockchainKey32 = null;
- byte[] deviceKey32 = null;
+ byte[] clientKey32 = null;
long paidLimitBytes = 0L;
List sessions = new ArrayList<>();
@@ -170,7 +170,7 @@ public final class SolanaUserPdaImportService {
if (blockType == 1) {
c += 32;
} else if (blockType == 2) {
- deviceKey32 = slice(raw, c, 32);
+ clientKey32 = slice(raw, c, 32);
c += 32;
} else if (blockType == 3) {
int count = u8(raw, c++);
@@ -245,12 +245,12 @@ public final class SolanaUserPdaImportService {
if (c > recordLen) return null;
}
- if (blockchainName == null || blockchainKey32 == null || deviceKey32 == null) return null;
+ if (blockchainName == null || blockchainKey32 == null || clientKey32 == null) return null;
return new ParsedSolanaUser(
login,
blockchainName,
Base64.getEncoder().encodeToString(blockchainKey32),
- Base64.getEncoder().encodeToString(deviceKey32),
+ Base64.getEncoder().encodeToString(clientKey32),
paidLimitBytes,
sessions
);
@@ -318,7 +318,7 @@ public final class SolanaUserPdaImportService {
String login,
String blockchainName,
String blockchainKeyB64,
- String deviceKeyB64,
+ String clientKeyB64,
long paidLimitBytes,
List sessions
) {}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/all_files.txt b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/all_files.txt
deleted file mode 100644
index d89a693..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/all_files.txt
+++ /dev/null
@@ -1,1439 +0,0 @@
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 авторизации: запрос выдачи одноразового nonce (authNonce).
- *
- * Клиент по логину просит сервер сгенерировать случайный authNonce,
- * который будет использован на втором шаге при подписи.
- *
- * Формат входящего JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "payload": {
- * "login": "someLogin"
- * }
- * }
- *
- * Формат успешного ответа:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Request extends Net_Request {
-
- /**
- * Логин пользователя, для которого запускается авторизация.
- */
- private String login;
-
- public String getLogin() {
- return login;
- }
- public void setLogin(String login) {
- this.login = login;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на AuthChallenge.
- *
- * При успехе сервер возвращает одноразовый nonce для подписи (authNonce),
- * который клиент обязан использовать на втором шаге при формировании строки
- * для цифровой подписи.
- *
- * JSON:
- * {
- * "op": "AuthChallenge",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "authNonce": "base64-строка-от-32-байт"
- * }
- * }
- */
-public class Net_AuthChallenge_Response extends Net_Response {
-
- /**
- * Одноразовый nonce для авторификации.
- * Строка — это base64-представление 32 случайных байт.
- */
- private String authNonce;
-
- public String getAuthNonce() {
- return authNonce;
- }
-
- public void setAuthNonce(String authNonce) {
- this.authNonce = authNonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос CloseActiveSession — закрытие активной сессии пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и "AUTH_IN_PROGRESS" здесь больше нет.
- *
- * payload:
- * {
- * "sessionId": "..." // опционально; если пусто — закрываем текущую
- * }
- */
-public class Net_CloseActiveSession_Request extends Net_Request {
-
- /** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CloseActiveSession.
- *
- * При успехе:
- * - status = 200;
- * - payload = {}.
- *
- * Закрытие WebSocket-соединения может быть выполнено сразу (для другой сессии)
- * или чуть позже (для текущей сессии) после отправки ответа.
- */
-public class Net_CloseActiveSession_Response extends Net_Response {
- // Дополнительных полей пока не требуется.
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
- *
- * Шаги:
- * 1) AuthChallenge(login) -> authNonce
- * 2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
- *
- * Подпись deviceKey делается над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
- *
- * Важно:
- * - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
- * - В БД active_sessions.session_key хранится sessionPubKeyB64.
- */
-public class Net_CreateAuthSession_Request extends Net_Request {
-
- /** Клиентский пароль для хранения данных (base64 от 32 байт). */
- private String storagePwd;
-
- /** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
- private String sessionPubKeyB64;
-
- /** Время на стороне клиента (мс с 1970-01-01). */
- private long timeMs;
-
- /** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-
- public String getSessionPubKeyB64() {
- return sessionPubKeyB64;
- }
-
- public void setSessionPubKeyB64(String sessionPubKeyB64) {
- this.sessionPubKeyB64 = sessionPubKeyB64;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на CreateAuthSession (v2).
- *
- * При успехе сервер создаёт запись в active_sessions
- * и возвращает идентификатор сессии sessionId.
- *
- * JSON:
- * {
- * "op": "CreateAuthSession",
- * "requestId": "...",
- * "status": 200,
- * "payload": {
- * "sessionId": "base64(32)"
- * }
- * }
- */
-public class Net_CreateAuthSession_Response extends Net_Response {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListSessions — список активных сессий пользователя.
- *
- * Новая логика (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Пустой payload.
- */
-public class Net_ListSessions_Request extends Net_Request {
- // пусто
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.List;
-
-/**
- * Ответ на ListSessions.
- *
- * При успехе:
- * - status = 200;
- * - payload:
- * {
- * "sessions": [
- * {
- * "sessionId": "...",
- * "clientInfoFromClient": "...",
- * "clientInfoFromRequest": "...",
- * "geo": "Country, City" | "unknown",
- * "lastAuthirificatedAtMs": 1733310000000
- * },
- * ...
- * ]
- * }
- */
-public class Net_ListSessions_Response extends Net_Response {
-
- /**
- * Список активных сессий для текущего пользователя.
- */
- private List sessions;
-
- public List getSessions() {
- return sessions;
- }
-
- public void setSessions(List sessions) {
- this.sessions = sessions;
- }
-
- /**
- * Описание одной активной сессии.
- */
- public static class SessionInfo {
-
- /** Идентификатор сессии, base64 от 32 байт. */
- private String sessionId;
-
- /** Что прислал клиент в CreateAuthSession/RefreshSession (clientInfo). */
- private String clientInfoFromClient;
-
- /** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
- private String clientInfoFromRequest;
-
- /** Строка геолокации вида "Country, City" или "unknown". */
- private String geo;
-
- /** Время последней успешной авторизации/refresh (мс с 1970-01-01). */
- private long lastAuthirificatedAtMs;
-
- // --- getters / setters ---
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- public String getClientInfoFromClient() {
- return clientInfoFromClient;
- }
-
- public void setClientInfoFromClient(String clientInfoFromClient) {
- this.clientInfoFromClient = clientInfoFromClient;
- }
-
- public String getClientInfoFromRequest() {
- return clientInfoFromRequest;
- }
-
- public void setClientInfoFromRequest(String clientInfoFromRequest) {
- this.clientInfoFromRequest = clientInfoFromRequest;
- }
-
- public String getGeo() {
- return geo;
- }
-
- public void setGeo(String geo) {
- this.geo = geo;
- }
-
- public long getLastAuthirificatedAtMs() {
- return lastAuthirificatedAtMs;
- }
-
- public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
- this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 1 входа в существующую сессию (v2):
- * SessionChallenge(sessionId) -> nonce
- */
-public class Net_SessionChallenge_Request extends Net_Request {
-
- private String sessionId;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionChallenge (v2).
- * payload: { "nonce": "base64(32)" }
- */
-public class Net_SessionChallenge_Response extends Net_Response {
-
- private String nonce;
-
- public String getNonce() {
- return nonce;
- }
-
- public void setNonce(String nonce) {
- this.nonce = nonce;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Шаг 2 входа в существующую сессию (v2):
- * SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
- *
- * Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- *
- * nonce берётся из SessionChallenge и хранится в ctx (одноразовый, TTL).
- */
-public class Net_SessionLogin_Request extends Net_Request {
-
- private String sessionId;
- private long timeMs;
- private String signatureB64;
-
- /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
- private String clientInfo;
-
- public String getSessionId() {
- return sessionId;
- }
-
- public void setSessionId(String sessionId) {
- this.sessionId = sessionId;
- }
-
- 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;
- }
-
- public String getClientInfo() {
- return clientInfo;
- }
-
- public void setClientInfo(String clientInfo) {
- this.clientInfo = clientInfo;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на SessionLogin (v2).
- * payload: { "storagePwd": "base64(32)" }
- */
-public class Net_SessionLogin_Response extends Net_Response {
-
- private String storagePwd;
-
- public String getStoragePwd() {
- return storagePwd;
- }
-
- public void setStoragePwd(String storagePwd) {
- this.storagePwd = storagePwd;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.SolanaUserEntry;
-
-import java.security.SecureRandom;
-
-/**
- * AuthChallenge (v2) — шаг 1 создания новой сессии.
- *
- * Логика авторизации (v2):
- * - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
- * - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
- * CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
- *
- * Что делает:
- * 1) Проверяет login.
- * 2) Находит пользователя (solana_users).
- * 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
- * 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
- */
-public class Net_AuthChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_AuthChallenge_Request req = (Net_AuthChallenge_Request) baseReq;
-
- String login = req.getLogin();
- if (login == null || login.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_LOGIN",
- "Пустой логин"
- );
- }
-
- // Если по этому соединению уже есть залогиненный пользователь — не даём повторную авторификацию
- if (ctx.getLogin() != null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "ALREADY_AUTHED",
- "Попытка повторной авторификации для уже заданного login=" + ctx.getLogin()
- );
- }
-
- SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
- if (solanaUserEntry == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "UNKNOWN_USER",
- "Пользователь с таким логином не найден"
- );
- }
-
- ctx.setSolanaUser(solanaUserEntry);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String authNonce = Base64Ws.encode(buf);
-
- ctx.setAuthNonce(authNonce);
-
- Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setAuthNonce(authNonce);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-
-import java.sql.SQLException;
-
-/**
- * CloseActiveSession (v2) — закрытие текущей или другой сессии.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей и AUTH_IN_PROGRESS здесь больше нет.
- *
- * Закрытие:
- * - удаляем запись из БД
- * - если по sessionId есть активный WS — закрываем его
- */
-public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- String targetSessionId = req.getSessionId();
- if (targetSessionId == null || targetSessionId.isBlank()) {
- if (ctx.getSessionId() != null && !ctx.getSessionId().isBlank()) {
- targetSessionId = ctx.getSessionId();
- } else if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
- targetSessionId = ctx.getActiveSession().getSessionId();
- } else {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_SESSION_TO_CLOSE",
- "Не удалось определить, какую сессию нужно закрыть"
- );
- }
- }
-
- ActiveSessionEntry targetSession;
- try {
- targetSession = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных при поиске сессии"
- );
- }
-
- if (targetSession == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия для закрытия не найдена"
- );
- }
-
- if (currentLogin == null || !currentLogin.equals(targetSession.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_OF_ANOTHER_USER",
- "Нельзя закрывать сессию другого пользователя"
- );
- }
-
- boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
-
- closeActiveSession(targetSessionId, ctx, isCurrentSession);
-
- Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- return resp;
- }
-
- private void closeActiveSession(String targetSessionId,
- ConnectionContext currentCtx,
- boolean isCurrentSession) {
-
- try {
- ActiveSessionsDAO.getInstance().deleteBySessionId(targetSessionId);
- } catch (SQLException e) {
- log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
- }
-
- ConnectionContext ctxToClose =
- ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
-
- if (ctxToClose == null) return;
-
- if (isCurrentSession && ctxToClose == currentCtx) {
- new Thread(() -> {
- try { Thread.sleep(50); } catch (InterruptedException ignored) {}
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }, "CloseSession-" + targetSessionId).start();
- } else {
- WsConnectionUtils.closeConnection(
- ctxToClose,
- 4000,
- "Session closed by client via CloseActiveSession"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import org.eclipse.jetty.websocket.api.Session;
-
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.sql.SQLException;
-
-/**
- * CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
- *
- * Логика авторизации (v2):
- * - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
- * - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
- * отправляет на сервер ТОЛЬКО sessionPubKeyB64.
- * - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
- *
- * Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
- * AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
- *
- * На выходе:
- * - создаётся запись active_sessions
- * - ctx становится AUTH_STATUS_USER (вход выполнен как "текущая сессия")
- * - ответ: sessionId
- */
-public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
- private static final SecureRandom RANDOM = new SecureRandom();
-
- public static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
-
- Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
-
- if (ctx == null
- || ctx.getSolanaUser() == null
- || ctx.getAuthNonce() == null
- || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
-
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_STEP1_CONTEXT",
- "Шаг 1 авторизации не был корректно выполнен для данного соединения"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no step1 context or bad auth state");
- return err;
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String login = user.getLogin();
- if (login == null || login.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_LOGIN",
- "Для пользователя не задан login в БД"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no login");
- return err;
- }
-
- String storagePwd = req.getStoragePwd();
- if (storagePwd == null || storagePwd.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_STORAGE_PWD",
- "Пустой storagePwd"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty storagePwd");
- return err;
- }
-
- String sessionPubKeyB64 = req.getSessionPubKeyB64();
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_PUBKEY",
- "Пустой sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
- return err;
- }
-
- // Проверим, что sessionPubKeyB64 декодируется в 32 байта
- byte[] sessionPubKey32;
- try {
- sessionPubKey32 = Base64Ws.decode(sessionPubKeyB64);
- } catch (IllegalArgumentException e) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный base64 в sessionPubKeyB64"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
- return err;
- }
- if (sessionPubKey32.length != 32) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_SESSION_PUBKEY_LEN",
- "sessionPubKey должен быть 32 байта"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
- return err;
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая цифровая подпись"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty signature");
- return err;
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- long diff = Math.abs(nowMs - timeMs);
- if (diff > ALLOWED_SKEW_MS) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: time skew");
- return err;
- }
-
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String devicePubKeyB64 = user.getDeviceKey();
- if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_DEVICE_KEY",
- "Отсутствует deviceKey у пользователя"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no deviceKey");
- return err;
- }
-
- String authNonce = ctx.getAuthNonce();
-
- boolean sigOk;
- try {
- sigOk = verifyCreateSessionSignature(
- user,
- login,
- authNonce,
- timeMs,
- signatureB64
- );
- } catch (IllegalArgumentException ex) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный формат Base64 для ключа или подписи"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad base64");
- return err;
- }
-
- if (!sigOk) {
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad signature");
- return err;
- }
-
- // --- генерируем sessionId ---
- String sessionId = generateRandom32B64Url();
- long now = System.currentTimeMillis();
-
- // --- Сбор данных о клиенте (IP, UA, язык) ---
- Session wsSession = ctx.getWsSession();
- String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
- String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);
-
- String clientIp = "";
- if (wsSession != null) {
- String ip = ClientInfoService.extractClientIp(wsSession);
- if (ip != null) clientIp = ip;
-
- if (!clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- // --- создаём запись ActiveSession и сохраняем в БД ---
- ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
- ActiveSessionEntry activeSessionEntry;
-
- try {
- activeSessionEntry = new ActiveSessionEntry(
- sessionId,
- login,
- sessionPubKeyB64, // session_key (pubkey)
- storagePwd,
- now,
- now,
- null, // pushEndpoint
- null, // pushP256dhKey
- null, // pushAuthKey
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
-
- dao.insert(activeSessionEntry);
- } catch (SQLException e) {
- log.error("Ошибка БД при создании новой сессии для login={}", login, e);
- Net_Response err = NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_SESSION_CREATE",
- "Ошибка БД при создании сессии"
- );
- WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db error");
- return err;
- }
-
- // --- обновляем контекст ---
- ctx.setActiveSession(activeSessionEntry);
- ctx.setSessionId(sessionId);
- ctx.setAuthNonce(null);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // --- формируем ответ ---
- Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessionId(sessionId);
- return resp;
- }
-
- private static boolean verifyCreateSessionSignature(
- SolanaUserEntry user,
- String login,
- String authNonce,
- long timeMs,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // deviceKey (pub, 32)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
- byte[] signature64 = Base64Ws.decode(signatureB64);
-
- String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-
- private static String generateRandom32B64Url() {
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- return Base64Ws.encode(buf);
- }
-}
-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.Net_Request;
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.GeoLookupService;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListSessions (v2) — список активных сессий.
- *
- * Логика авторизации (v2):
- * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
- * - Никаких подписей здесь больше нет.
- */
-public class Net_ListSessions_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListSessions_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
-
- if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "NOT_AUTHENTICATED",
- "Операция доступна только для авторизованных пользователей"
- );
- }
-
- SolanaUserEntry user = ctx.getSolanaUser();
- String currentLogin = user.getLogin();
-
- List sessions;
- try {
- sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
- } catch (SQLException e) {
- log.error("Ошибка БД при получении списка сессий для login={}", currentLogin, e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_LIST_SESSIONS",
- "Ошибка доступа к базе данных при получении списка сессий"
- );
- }
-
- List resultList = new ArrayList<>();
- for (ActiveSessionEntry s : sessions) {
- SessionInfo info = new SessionInfo();
- info.setSessionId(s.getSessionId());
- info.setClientInfoFromClient(s.getClientInfoFromClient());
- info.setClientInfoFromRequest(s.getClientInfoFromRequest());
- info.setLastAuthirificatedAtMs(s.getLastAuthirificatedAtMs());
-
- String ip = s.getClientIp();
- String geo = GeoLookupService.resolveCountryCityOrIpWithCache(ip);
- info.setGeo(geo);
-
- resultList.add(info);
- }
-
- Net_ListSessions_Response resp = new Net_ListSessions_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setSessions(resultList);
-
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import server.logic.ws_protocol.Base64Ws;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.dao.ActiveSessionsDAO;
-import shine.db.entities.ActiveSessionEntry;
-
-import java.security.SecureRandom;
-import java.sql.SQLException;
-
-/**
- * SessionChallenge (v2) — шаг 1 входа в существующую сессию.
- *
- * Логика авторизации (v2):
- * - Вход в существующую сессию ВСЕГДА в 2 шага:
- * 1) SessionChallenge(sessionId) -> nonce
- * 2) SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
- *
- * Что делает:
- * - Проверяет, что sessionId существует в БД.
- * - Генерирует одноразовый nonce (base64url(32)), сохраняет его в ctx:
- * ctx.sessionLoginNonce, ctx.sessionLoginSessionId, ctx.sessionLoginNonceExpiresAtMs.
- */
-public class Net_SessionChallenge_Handler implements JsonMessageHandler {
-
- private static final SecureRandom RANDOM = new SecureRandom();
- private static final long NONCE_TTL_MS = 60_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionChallenge_Request req = (Net_SessionChallenge_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- byte[] buf = new byte[32];
- RANDOM.nextBytes(buf);
- String nonce = Base64Ws.encode(buf);
-
- long now = System.currentTimeMillis();
- ctx.setSessionLoginNonce(nonce);
- ctx.setSessionLoginSessionId(sessionId);
- ctx.setSessionLoginNonceExpiresAtMs(now + NONCE_TTL_MS);
-
- Net_SessionChallenge_Response resp = new Net_SessionChallenge_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setNonce(nonce);
- return resp;
- }
-}
-package server.logic.ws_protocol.JSON.handlers.auth;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.Base64Ws;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;
-import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
-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.ActiveSessionEntry;
-import shine.db.entities.SolanaUserEntry;
-import shine.geo.ClientInfoService;
-import shine.geo.GeoLookupService;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.SQLException;
-
-/**
- * SessionLogin (v2) — шаг 2 входа в существующую сессию (по sessionKey).
- *
- * Логика авторизации (v2):
- * - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
- * - SessionLogin проверяет подпись sessionKey над строкой:
- * SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
- * - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
- *
- * При успехе:
- * - ctx становится AUTH_STATUS_USER
- * - обновляем метаданные сессии (lastAuth + clientIp + clientInfo + lang)
- * - возвращаем storagePwd
- */
-public class Net_SessionLogin_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
-
- private static final long ALLOWED_SKEW_MS = 30_000L;
-
- @Override
- public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
- Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;
-
- String sessionId = req.getSessionId();
- if (sessionId == null || sessionId.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SESSION_ID",
- "Пустой sessionId"
- );
- }
-
- // проверка челленджа
- if (ctx.getSessionLoginNonce() == null
- || ctx.getSessionLoginSessionId() == null
- || System.currentTimeMillis() > ctx.getSessionLoginNonceExpiresAtMs()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "NO_CHALLENGE",
- "Нет активного SessionChallenge или nonce истёк"
- );
- }
-
- if (!sessionId.equals(ctx.getSessionLoginSessionId())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "SESSION_ID_MISMATCH",
- "nonce был выдан для другого sessionId"
- );
- }
-
- long timeMs = req.getTimeMs();
- long nowMs = System.currentTimeMillis();
- if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "TIME_SKEW",
- "Время клиента отличается от сервера более чем на 30 секунд"
- );
- }
-
- String signatureB64 = req.getSignatureB64();
- if (signatureB64 == null || signatureB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "EMPTY_SIGNATURE",
- "Пустая подпись"
- );
- }
-
- ActiveSessionEntry session;
- try {
- session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка доступа к базе данных"
- );
- }
-
- if (session == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "SESSION_NOT_FOUND",
- "Сессия не найдена"
- );
- }
-
- String sessionPubKeyB64 = session.getSessionKey(); // это pubKey (Base64(32))
- if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "NO_SESSION_KEY",
- "В сессии не задан session_key"
- );
- }
-
- String nonce = ctx.getSessionLoginNonce();
-
- boolean sigOk;
- try {
- sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "Некорректный Base64 для ключа/подписи"
- );
- }
-
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "BAD_SIGNATURE",
- "Подпись не прошла проверку"
- );
- }
-
- // сжигаем nonce
- ctx.setSessionLoginNonce(null);
- ctx.setSessionLoginSessionId(null);
- ctx.setSessionLoginNonceExpiresAtMs(0);
-
- // подтягиваем пользователя
- SolanaUserEntry user;
- try {
- user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
- } catch (SQLException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR_USER_LOOKUP",
- "Ошибка доступа к базе данных при получении пользователя"
- );
- }
-
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.UNVERIFIED,
- "USER_NOT_FOUND_FOR_SESSION",
- "Пользователь для данной сессии не найден"
- );
- }
-
- // обновление метаданных
- String clientInfoFromClient = req.getClientInfo();
- if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
- clientInfoFromClient = clientInfoFromClient.substring(0, 50);
- }
-
- String clientIp = null;
- String clientInfoFromRequest = null;
- String userLanguage = null;
-
- if (ctx.getWsSession() != null) {
- clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
- clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
- userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());
-
- if (clientIp != null && !clientIp.isBlank()) {
- try {
- GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
- } catch (Exception e) {
- log.debug("Geo lookup failed for ip={}", clientIp, e);
- }
- }
- }
-
- long now = System.currentTimeMillis();
- try {
- ActiveSessionsDAO.getInstance().updateOnRefresh(
- sessionId,
- now,
- clientIp,
- clientInfoFromClient,
- clientInfoFromRequest,
- userLanguage
- );
- } catch (SQLException e) {
- log.error("Ошибка БД при updateOnRefresh sessionId={}", sessionId, e);
- }
-
- session.setLastAuthirificatedAtMs(now);
- session.setClientIp(clientIp);
- session.setClientInfoFromClient(clientInfoFromClient);
- session.setClientInfoFromRequest(clientInfoFromRequest);
- session.setUserLanguage(userLanguage);
-
- // ctx
- ctx.setActiveSession(session);
- ctx.setSolanaUser(user);
- ctx.setSessionId(sessionId);
- ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
-
- ActiveConnectionsRegistry.getInstance().register(ctx);
-
- // ответ
- Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
- resp.setStoragePwd(session.getStoragePwd());
- return resp;
- }
-
- private static boolean verifySessionLoginSignature(
- String sessionPubKeyB64,
- String sessionId,
- long timeMs,
- String nonce,
- String signatureB64
- ) throws IllegalArgumentException {
-
- // pubKey: Base64(32). (Ed25519Util.keyFromBase64 должен использовать стандартный Base64)
- byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);
-
- // signature: Base64(64) через единую утилиту WS-протокола
- byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
-
- String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
-
- return Ed25519Util.verify(preimage, signature64, publicKey32);
- }
-}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/AUTH_SESSION_NEW.md b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/AUTH_SESSION_NEW.md
index a15fab3..9e6ab85 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/AUTH_SESSION_NEW.md
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/AUTH_SESSION_NEW.md
@@ -35,7 +35,7 @@
1. Добавление пользователя (AddUser)
-Назначение: создать локальную запись пользователя с двумя ключами — solanaKey и deviceKey.
+Назначение: создать локальную запись пользователя с двумя ключами — solanaKey и clientKey.
📤 Запрос клиента
{
@@ -46,7 +46,7 @@
"loginId": 100212,
"bchId": 4222,
"solanaKey": "BASE64_LOGIN_KEY",
-"deviceKey": "BASE64_DEVICE_KEY",
+"clientKey": "BASE64_DEVICE_KEY",
"bchLimit": 1000000
}
}
@@ -62,7 +62,7 @@ login TEXT NOT NULL,
loginId INTEGER PRIMARY KEY,
bchId INTEGER NOT NULL,
solanaKey TEXT,
-deviceKey TEXT,
+clientKey TEXT,
bchLimit INTEGER
);
@@ -118,7 +118,7 @@ timeMs — timestamp клиента (UTC).
sessionPwd — строка с шага 1.
-signatureB64 — Ed25519‐подпись preimage приватным ключом deviceKey.
+signatureB64 — Ed25519‐подпись preimage приватным ключом clientKey.
📤 Запрос клиента
{
@@ -141,7 +141,7 @@ signatureB64 — Ed25519‐подпись preimage приватным ключо
Восстанавливает preimage.
-Находит deviceKey пользователя.
+Находит clientKey пользователя.
Проверяет Ed25519-подпись.
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Request.java
index 91fa95b..cd4f99b 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Request.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Request.java
@@ -3,13 +3,13 @@ package server.logic.ws_protocol.JSON.handlers.auth.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
/**
- * Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
+ * Шаг 2 (v2): создание новой сессии ТОЛЬКО через clientKey.
*
* Шаги:
* 1) AuthChallenge(login) -> authNonce
- * 2) CreateAuthSession(login, sessionKey, storagePwd, timeMs, authNonce, deviceKey, signatureB64, clientInfo)
+ * 2) CreateAuthSession(login, sessionKey, storagePwd, timeMs, authNonce, clientKey, signatureB64, clientInfo)
*
- * Подпись deviceKey делается над строкой (UTF-8):
+ * Подпись clientKey делается над строкой (UTF-8):
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
*
* Важно:
@@ -33,9 +33,9 @@ public class Net_CreateAuthSession_Request extends Net_Request {
private String authNonce;
/** Публичный ключ устройства пользователя. */
- private String deviceKey;
+ private String clientKey;
- /** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
+ /** Подпись Ed25519(clientKey) над строкой AUTH_CREATE_SESSION:... (base64). */
private String signatureB64;
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
@@ -87,12 +87,12 @@ public class Net_CreateAuthSession_Request extends Net_Request {
this.authNonce = authNonce;
}
- public String getDeviceKey() {
- return deviceKey;
+ public String getClientKey() {
+ return clientKey;
}
- public void setDeviceKey(String deviceKey) {
- this.deviceKey = deviceKey;
+ public void setClientKey(String clientKey) {
+ this.clientKey = clientKey;
}
public String getSignatureB64() {
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/all_files.txt b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/all_files.txt
deleted file mode 100644
index 430f54f..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/all_files.txt
+++ /dev/null
@@ -1,180 +0,0 @@
-package server.logic.ws_protocol.JSON.handlers.connections.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetFriendsLists — получить два списка "друзей" по connections_state.
- *
- * {
- * "op": "GetFriendsLists",
- * "requestId": "req-100",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * Возвращает:
- * - out_friends: кому login поставил FRIEND
- * - in_friends: кто поставил FRIEND этому login
- *
- * ПРО ДОСТУП (на будущее):
- * Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей.
- */
-public class Net_GetFriendsLists_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.connections.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ GetFriendsLists.
- *
- * {
- * "op": "GetFriendsLists",
- * "requestId": "req-100",
- * "status": 200,
- * "payload": {
- * "login": "Anya", // канонический регистр из БД
- * "out_friends": ["Bob", "Kate"], // кому login поставил FRIEND
- * "in_friends": ["Alex", "Kate"] // кто поставил FRIEND login
- * }
- * }
- */
-public class Net_GetFriendsLists_Response extends Net_Response {
-
- private String login;
-
- private List out_friends = new ArrayList<>();
- private List in_friends = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List getOut_friends() { return out_friends; }
- public void setOut_friends(List out_friends) { this.out_friends = out_friends; }
-
- public List getIn_friends() { return in_friends; }
- public void setIn_friends(List in_friends) { this.in_friends = in_friends; }
-}
-package server.logic.ws_protocol.JSON.handlers.connections;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
-import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.MsgSubType;
-import shine.db.SqliteDbController;
-import shine.db.dao.ConnectionsStateDAO;
-
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.util.List;
-
-/**
- * GetFriendsLists — получить 2 списка:
- * - out_friends: кому login поставил FRIEND
- * - in_friends: кто поставил FRIEND этому login
- *
- * ВАЖНО:
- * - login в запросе может быть любым регистром
- * - в ответе возвращаем канонический регистр (как в solana_users.login)
- *
- * ПРИМЕЧАНИЕ:
- * Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
- */
-public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- final String loginAnyCase = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
-
- // 1) Канонизируем login через solana_users (NOCASE)
- String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
- if (canonicalLogin == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- int relType = (int) MsgSubType.CONNECTION_FRIEND;
-
- // 2) Два списка (логины канонические)
- List outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType);
- List inFriends = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType);
-
- Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(canonicalLogin);
- resp.setOut_friends(outFriends);
- resp.setIn_friends(inFriends);
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetFriendsLists", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-
- private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
- String sql = """
- SELECT login
- FROM solana_users
- WHERE login = ? COLLATE NOCASE
- LIMIT 1
- """;
- try (PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setString(1, loginAnyCase);
- try (ResultSet rs = ps.executeQuery()) {
- if (!rs.next()) return null;
- return rs.getString("login");
- }
- }
- }
-}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java
index 99b0405..9e28295 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java
@@ -62,7 +62,7 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
resp.setBlockchainName(u.getBlockchainName());
resp.setSolanaKey(u.getSolanaKey());
resp.setBlockchainKey(u.getBlockchainKey());
- resp.setDeviceKey(u.getDeviceKey());
+ resp.setClientKey(u.getClientKey());
// Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна,
// автоматически восстанавливаем её для существующего пользователя.
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/all_files.txt b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/all_files.txt
deleted file mode 100644
index f226a58..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/all_files.txt
+++ /dev/null
@@ -1,240 +0,0 @@
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос AddUser — временная/тестовая регистрация локального пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "payload": {
- * "login": "anya",
- * "blockchainName": "anya-001",
- * "solanaKey": "base64-ed25519-public-key-login",
- * "blockchainKey": "base64-ed25519-public-key-blockchain",
- * "deviceKey": "base64-ed25519-public-key-device",
- * "bchLimit": 1000000
- * }
- * }
- *
- * Все поля лежат внутри payload.
- */
-public class Net_AddUser_Request extends Net_Request {
-
- private String login;
- private String blockchainName;
-
- /** Ключ пользователя Solana (публичный ключ логина) */
- private String solanaKey;
-
- /** Ключ блокчейна (публичный ключ блокчейна) */
- private String blockchainKey;
-
- /** Ключ устройства (публичный ключ устройства) */
- private String deviceKey;
-
- private Integer bchLimit;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getBlockchainName() { return blockchainName; }
- public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
-
- public String getSolanaKey() { return solanaKey; }
- public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
-
- public String getBlockchainKey() { return blockchainKey; }
- public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
-
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
-
- public Integer getBchLimit() { return bchLimit; }
- public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Успешный ответ на AddUser.
- *
- * Сейчас дополнительных полей нет — достаточно status=200.
- *
- * Пример:
- * {
- * "op": "AddUser",
- * "requestId": "test-add-1",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_AddUser_Response extends Net_Response {
- // При необходимости сюда можно добавить, например, флаг created/updated и т.п.
-}
-package server.logic.ws_protocol.JSON.handlers.tempToTest;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
-import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.entities.BlockchainStateEntry;
-import shine.db.entities.SolanaUserEntry;
-import utils.blockchain.BlockchainNameUtil;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Base64;
-
-public class Net_AddUser_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
-
- /** TEST ONLY */
- private static final int TEST_BCH_LIMIT = 1_000_000;
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getBlockchainName() == null || req.getBlockchainName().isBlank()
- || req.getSolanaKey() == null || req.getSolanaKey().isBlank()
- || req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
- || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
- );
- }
-
- // blockchainName должен быть вида: -NNN
- if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BLOCKCHAIN_NAME",
- "blockchainName должен быть вида -NNN (пример: anya-001)"
- );
- }
-
- int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0)
- ? TEST_BCH_LIMIT
- : req.getBchLimit();
-
- try {
- byte[] blockchainKey32 = Base64.getDecoder().decode(req.getBlockchainKey());
- if (blockchainKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BLOCKCHAIN_KEY",
- "blockchainKey должен быть Base64(32 bytes)"
- );
- }
-
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- SqliteDbController db = SqliteDbController.getInstance();
-
- try (Connection c = db.getConnection()) {
- c.setAutoCommit(false);
-
- // 1. Проверяем, что пользователя нет
- if (usersDAO.getByLogin(req.getLogin()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "USER_ALREADY_EXISTS",
- "Пользователь с таким login уже существует"
- );
- }
-
- // 2. Проверяем, что blockchain_state ещё нет
- if (stateDAO.getByBlockchainName(req.getBlockchainName()) != null) {
- return NetExceptionResponseFactory.error(
- req,
- 409,
- "BLOCKCHAIN_ALREADY_EXISTS",
- "blockchain_state уже существует"
- );
- }
-
- // 3. Создаём пользователя (solanaKey + deviceKey)
- SolanaUserEntry user = new SolanaUserEntry(
- req.getLogin(),
- req.getSolanaKey(),
- req.getDeviceKey()
- );
-
- usersDAO.insert(c, user);
-
- // 4. Создаём INITIAL blockchain_state (blockchainKey)
- BlockchainStateEntry st = new BlockchainStateEntry();
- st.setBlockchainName(req.getBlockchainName());
- st.setLogin(req.getLogin());
- st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
- st.setLastBlockNumber(-1);
- st.setLastBlockHash(new byte[32]);
- st.setFileSizeBytes(0);
- st.setSizeLimit(limit);
- st.setUpdatedAtMs(System.currentTimeMillis());
-
- stateDAO.upsert(c, st);
-
- c.commit();
- }
-
- Net_AddUser_Response resp = new Net_AddUser_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
- req.getLogin(), req.getBlockchainName(), limit);
-
- return resp;
-
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_KEY_FORMAT",
- e.getMessage()
- );
- } catch (SQLException e) {
- log.error("❌ DB error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error AddUser", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Request.java
index 0a7ea45..28b6a6b 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Request.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Request.java
@@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
* "blockchainName": "anya-001",
* "solanaKey": "base64-ed25519-public-key-login",
* "blockchainKey": "base64-ed25519-public-key-blockchain",
- * "deviceKey": "base64-ed25519-public-key-device",
+ * "clientKey": "base64-ed25519-public-key-device",
* "bchLimit": 1000000
* }
* }
@@ -34,7 +34,7 @@ public class Net_AddUser_Request extends Net_Request {
private String blockchainKey;
/** Ключ устройства (публичный ключ устройства) */
- private String deviceKey;
+ private String clientKey;
private Integer bchLimit;
@@ -50,8 +50,8 @@ public class Net_AddUser_Request extends Net_Request {
public String getBlockchainKey() { return blockchainKey; }
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
+ public String getClientKey() { return clientKey; }
+ public void setClientKey(String clientKey) { this.clientKey = clientKey; }
public Integer getBchLimit() { return bchLimit; }
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java
index f5ce1e6..0037ed2 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java
@@ -26,7 +26,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
* "blockchainName": "anya-001",
* "solanaKey": "...",
* "blockchainKey": "...",
- * "deviceKey": "..."
+ * "clientKey": "..."
* }
* }
*/
@@ -38,7 +38,7 @@ public class Net_GetUser_Response extends Net_Response {
private String blockchainName;
private String solanaKey;
private String blockchainKey;
- private String deviceKey;
+ private String clientKey;
private Integer serverLastGlobalNumber;
private String serverLastGlobalHash;
private Long serverBlockchainSizeBytes;
@@ -59,8 +59,8 @@ public class Net_GetUser_Response extends Net_Response {
public String getBlockchainKey() { return blockchainKey; }
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
- public String getDeviceKey() { return deviceKey; }
- public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
+ public String getClientKey() { return clientKey; }
+ public void setClientKey(String clientKey) { this.clientKey = clientKey; }
public Integer getServerLastGlobalNumber() { return serverLastGlobalNumber; }
public void setServerLastGlobalNumber(Integer serverLastGlobalNumber) { this.serverLastGlobalNumber = serverLastGlobalNumber; }
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_GetUserParam_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_GetUserParam_Handler.java
index 6179402..93c1298 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_GetUserParam_Handler.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_GetUserParam_Handler.java
@@ -71,7 +71,7 @@ public class Net_GetUserParam_Handler implements JsonMessageHandler {
resp.setParam(e.getParam());
resp.setTime_ms(e.getTimeMs());
resp.setValue(e.getValue());
- resp.setDevice_key(e.getDeviceKey());
+ resp.setClient_key(e.getClientKey());
resp.setSignature(e.getSignature());
return resp;
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_ListUserParams_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_ListUserParams_Handler.java
index c70c79f..866a9c5 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_ListUserParams_Handler.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_ListUserParams_Handler.java
@@ -73,7 +73,7 @@ public class Net_ListUserParams_Handler implements JsonMessageHandler {
it.setParam(e.getParam());
it.setTime_ms(e.getTimeMs());
it.setValue(e.getValue());
- it.setDevice_key(e.getDeviceKey());
+ it.setClient_key(e.getClientKey());
it.setSignature(e.getSignature());
items.add(it);
}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_UpsertUserParam_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_UpsertUserParam_Handler.java
index 1b4b019..1d1f369 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_UpsertUserParam_Handler.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/Net_UpsertUserParam_Handler.java
@@ -28,8 +28,8 @@ import java.sql.SQLException;
*
* Делает (MVP, без "сессий"):
* 1) Проверка входных полей.
- * 2) Проверка подписи Ed25519 по device_key.
- * 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
+ * 2) Проверка подписи Ed25519 по client_key.
+ * 3) Проверка, что пользователь существует и что client_key принадлежит этому login.
* 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
*
* ВАЖНО:
@@ -50,14 +50,14 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|| req.getParam() == null || req.getParam().isBlank()
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|| req.getValue() == null
- || req.getDevice_key() == null || req.getDevice_key().isBlank()
+ || req.getClient_key() == null || req.getClient_key().isBlank()
|| req.getSignature() == null || req.getSignature().isBlank()) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.BAD_REQUEST,
"BAD_FIELDS",
- "Некорректные поля: login/param/time_ms/value/device_key/signature"
+ "Некорректные поля: login/param/time_ms/value/client_key/signature"
);
}
@@ -65,7 +65,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
final String param = req.getParam().trim();
final long timeMs = req.getTime_ms();
final String value = req.getValue();
- final String deviceKeyB64 = req.getDevice_key().trim();
+ final String clientKeyB64 = req.getClient_key().trim();
final String signatureB64 = req.getSignature().trim();
try {
@@ -73,14 +73,14 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
byte[] pubKey32;
byte[] sig64;
try {
- pubKey32 = Base64Ws.decodeLen(deviceKeyB64, 32, "device_key");
+ pubKey32 = Base64Ws.decodeLen(clientKeyB64, 32, "client_key");
sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
} catch (IllegalArgumentException e) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.BAD_REQUEST,
"BAD_BASE64",
- "device_key/signature должны быть Base64"
+ "client_key/signature должны быть Base64"
);
}
@@ -120,23 +120,23 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
);
}
- // 2) device key must match the user's stored deviceKey
- String userDeviceKey = user.getDeviceKey();
- if (userDeviceKey == null || userDeviceKey.isBlank()) {
+ // 2) client key must match the user's stored clientKey
+ String userClientKey = user.getClientKey();
+ if (userClientKey == null || userClientKey.isBlank()) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.SERVER_DATA_ERROR,
"USER_DEVICE_KEY_EMPTY",
- "У пользователя не задан deviceKey в БД"
+ "У пользователя не задан clientKey в БД"
);
}
- if (!userDeviceKey.trim().equals(deviceKeyB64)) {
+ if (!userClientKey.trim().equals(clientKeyB64)) {
return NetExceptionResponseFactory.error(
req,
403,
"DEVICE_KEY_MISMATCH",
- "device_key не соответствует пользователю"
+ "client_key не соответствует пользователю"
);
}
@@ -146,7 +146,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
param,
timeMs,
value,
- deviceKeyB64,
+ clientKeyB64,
signatureB64
);
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/all_files.txt b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/all_files.txt
deleted file mode 100644
index 4db8a35..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/all_files.txt
+++ /dev/null
@@ -1,640 +0,0 @@
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос GetUserParam — получить один параметр пользователя.
- *
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) этот запрос не ограничивает просмотр параметров, т.к. проект в тестовом режиме.
- * Позже, вероятно, потребуется ограничить: кто и какие параметры может читать (сессия/права).
- * Но для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ GetUserParam.
- *
- * Если найден:
- * {
- * "op": "GetUserParam",
- * "requestId": "req-1",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * }
- * }
- *
- * Если не найден:
- * status=404, payload пустой.
- */
-public class Net_GetUserParam_Response extends Net_Response {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос ListUserParams — получить все сохранённые параметры пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "payload": {
- * "login": "anya"
- * }
- * }
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Request extends Net_Request {
-
- private String login;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ответ ListUserParams — список всех параметров пользователя.
- *
- * {
- * "op": "ListUserParams",
- * "requestId": "req-2",
- * "status": 200,
- * "payload": {
- * "login": "anya",
- * "params": [
- * {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-32",
- * "signature": "base64-64"
- * },
- * ...
- * ]
- * }
- * }
- */
-public class Net_ListUserParams_Response extends Net_Response {
-
- private String login;
- private List
- params = new ArrayList<>();
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public List
- getParams() { return params; }
- public void setParams(List
- params) { this.params = params; }
-
- public static class Item {
- private String login;
- private String param;
- private Long time_ms;
- private String value;
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Request;
-
-/**
- * Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
- *
- * Клиент отправляет:
- *
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "payload": {
- * "login": "anya",
- * "param": "feed:lastSeenGlobal",
- * "time_ms": 1736000000123,
- * "value": "105",
- * "device_key": "base64-ed25519-public-key-32",
- * "signature": "base64-ed25519-signature-64"
- * }
- * }
- *
- * Подпись считается от UTF-8 строки:
- * USER_PARAMETER_PREFIX + login + param + time_ms + value
- */
-public class Net_UpsertUserParam_Request extends Net_Request {
-
- private String login;
- private String param;
- private Long time_ms;
- private String value;
-
- private String device_key;
- private String signature;
-
- public String getLogin() { return login; }
- public void setLogin(String login) { this.login = login; }
-
- public String getParam() { return param; }
- public void setParam(String param) { this.param = param; }
-
- public Long getTime_ms() { return time_ms; }
- public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
-
- public String getValue() { return value; }
- public void setValue(String value) { this.value = value; }
-
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
-
- public String getSignature() { return signature; }
- public void setSignature(String signature) { this.signature = signature; }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
-
-import server.logic.ws_protocol.JSON.entyties.Net_Response;
-
-/**
- * Ответ на UpsertUserParam.
- *
- * Успех:
- * {
- * "op": "UpsertUserParam",
- * "requestId": "req-123",
- * "status": 200,
- * "payload": { }
- * }
- */
-public class Net_UpsertUserParam_Response extends Net_Response {
- // MVP: без payload. При желании позже можно добавить created/updated.
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-
-/**
- * GetUserParam — получить один параметр пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_GetUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_GetUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_GetUserParam_Request req = (Net_GetUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param"
- );
- }
-
- String login = req.getLogin().trim();
- String param = req.getParam().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- UserParamEntry e = dao.getByLoginAndParam(c, login, param);
-
- if (e == null) {
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(404);
- return resp;
- }
-
- Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(e.getLogin());
- resp.setParam(e.getParam());
- resp.setTime_ms(e.getTimeMs());
- resp.setValue(e.getValue());
- resp.setDevice_key(e.getDeviceKey());
- resp.setSignature(e.getSignature());
-
- return resp;
- }
-
- } catch (Exception e) {
- log.error("❌ Internal error GetUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.UserParamEntry;
-
-import java.sql.Connection;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ListUserParams — получить все параметры пользователя.
- *
- * ПРО ДОСТУП (на будущее):
- * ---------------------------------------------------------------------------------
- * Сейчас (MVP) запрос не ограничивает просмотр параметров.
- * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
- * Для MVP эти проверки не нужны.
- * ---------------------------------------------------------------------------------
- */
-public class Net_ListUserParams_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_ListUserParams_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_ListUserParams_Request req = (Net_ListUserParams_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login"
- );
- }
-
- String login = req.getLogin().trim();
-
- try {
- SqliteDbController db = SqliteDbController.getInstance();
- UserParamsDAO dao = UserParamsDAO.getInstance();
-
- List entries;
- try (Connection c = db.getConnection()) {
- entries = dao.getByLogin(c, login);
- }
-
- Net_ListUserParams_Response resp = new Net_ListUserParams_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- resp.setLogin(login);
-
- List items = new ArrayList<>();
- for (UserParamEntry e : entries) {
- Net_ListUserParams_Response.Item it = new Net_ListUserParams_Response.Item();
- it.setLogin(e.getLogin());
- it.setParam(e.getParam());
- it.setTime_ms(e.getTimeMs());
- it.setValue(e.getValue());
- it.setDevice_key(e.getDeviceKey());
- it.setSignature(e.getSignature());
- items.add(it);
- }
- resp.setParams(items);
-
- return resp;
-
- } catch (Exception e) {
- log.error("❌ Internal error ListUserParams", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
-package server.logic.ws_protocol.JSON.handlers.userParams;
-
-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.handlers.JsonMessageHandler;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
-import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
-import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
-import server.logic.ws_protocol.WireCodes;
-import shine.db.SqliteDbController;
-import shine.db.dao.SolanaUsersDAO;
-import shine.db.dao.UserParamsDAO;
-import shine.db.entities.SolanaUserEntry;
-import shine.db.entities.UserParamEntry;
-import utils.config.ShineSignatureConstants;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Base64;
-
-/**
- * Net_UpsertUserParam_Handler
- *
- * Делает (MVP, без "сессий"):
- * 1) Проверка входных полей.
- * 2) Проверка подписи Ed25519 по device_key.
- * 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
- * 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
- *
- * ВАЖНО:
- * - НИКАКИХ ручных транзакций / BEGIN здесь нет.
- * - autoCommit=true, каждый statement завершённый сам по себе.
- * - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
- * наш финальный UPSERT просто вернёт 0 обновлённых строк.
- */
-public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
-
- private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);
-
- @Override
- public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
- Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;
-
- if (req.getLogin() == null || req.getLogin().isBlank()
- || req.getParam() == null || req.getParam().isBlank()
- || req.getTime_ms() == null || req.getTime_ms() <= 0
- || req.getValue() == null
- || req.getDevice_key() == null || req.getDevice_key().isBlank()
- || req.getSignature() == null || req.getSignature().isBlank()) {
-
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_FIELDS",
- "Некорректные поля: login/param/time_ms/value/device_key/signature"
- );
- }
-
- final String login = req.getLogin().trim();
- final String param = req.getParam().trim();
- final long timeMs = req.getTime_ms();
- final String value = req.getValue();
- final String deviceKeyB64 = req.getDevice_key().trim();
- final String signatureB64 = req.getSignature().trim();
-
- try {
- // ---------------- Base64 decode ----------------
- byte[] pubKey32;
- byte[] sig64;
- try {
- pubKey32 = Base64.getDecoder().decode(deviceKeyB64);
- sig64 = Base64.getDecoder().decode(signatureB64);
- } catch (IllegalArgumentException e) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_BASE64",
- "device_key/signature должны быть Base64"
- );
- }
-
- if (pubKey32.length != 32) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_DEVICE_KEY",
- "device_key должен быть Base64(32 bytes)"
- );
- }
- if (sig64.length != 64) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.BAD_REQUEST,
- "BAD_SIGNATURE",
- "signature должна быть Base64(64 bytes)"
- );
- }
-
- // ---------------- Signature verify ----------------
- String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
- + login
- + param
- + timeMs
- + value;
-
- byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
-
- boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
- if (!sigOk) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "SIGNATURE_INVALID",
- "Подпись не прошла проверку"
- );
- }
-
- // ---------------- DB checks + upsert ----------------
- SqliteDbController db = SqliteDbController.getInstance();
- SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
- UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
-
- try (Connection c = db.getConnection()) {
- // 1) user exists
- SolanaUserEntry user = usersDAO.getByLogin(c, login);
- if (user == null) {
- return NetExceptionResponseFactory.error(
- req,
- 404,
- "USER_NOT_FOUND",
- "Пользователь не найден"
- );
- }
-
- // 2) device key must match the user's stored deviceKey
- String userDeviceKey = user.getDeviceKey();
- if (userDeviceKey == null || userDeviceKey.isBlank()) {
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "USER_DEVICE_KEY_EMPTY",
- "У пользователя не задан deviceKey в БД"
- );
- }
-
- if (!userDeviceKey.trim().equals(deviceKeyB64)) {
- return NetExceptionResponseFactory.error(
- req,
- 403,
- "DEVICE_KEY_MISMATCH",
- "device_key не соответствует пользователю"
- );
- }
-
- // 3) atomic upsert-if-newer
- UserParamEntry e = new UserParamEntry(
- login,
- param,
- timeMs,
- value,
- deviceKeyB64,
- signatureB64
- );
-
- int changed = paramsDAO.upsertIfNewer(c, e);
-
- Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
- resp.setOp(req.getOp());
- resp.setRequestId(req.getRequestId());
- resp.setStatus(WireCodes.Status.OK);
-
- if (changed == 1) {
- log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
- } else {
- // 0 строк — значит в БД уже есть time_ms >= incoming
- log.info("ℹ️ UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
- }
-
- return resp;
- }
-
- } catch (SQLException e) {
- log.error("❌ DB error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.SERVER_DATA_ERROR,
- "DB_ERROR",
- "Ошибка БД"
- );
- } catch (Exception e) {
- log.error("❌ Internal error UpsertUserParam", e);
- return NetExceptionResponseFactory.error(
- req,
- WireCodes.Status.INTERNAL_ERROR,
- "INTERNAL_ERROR",
- "Внутренняя ошибка сервера"
- );
- }
- }
-}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/concat_to_file.sh b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_GetUserParam_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_GetUserParam_Response.java
index 77e5625..e5cb295 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_GetUserParam_Response.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_GetUserParam_Response.java
@@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
* "param": "feed:lastSeenGlobal",
* "time_ms": 1736000000123,
* "value": "105",
- * "device_key": "base64-32",
+ * "client_key": "base64-32",
* "signature": "base64-64"
* }
* }
@@ -29,7 +29,7 @@ public class Net_GetUserParam_Response extends Net_Response {
private String param;
private Long time_ms;
private String value;
- private String device_key;
+ private String client_key;
private String signature;
public String getLogin() { return login; }
@@ -44,8 +44,8 @@ public class Net_GetUserParam_Response extends Net_Response {
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
+ public String getClient_key() { return client_key; }
+ public void setClient_key(String client_key) { this.client_key = client_key; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_ListUserParams_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_ListUserParams_Response.java
index 75e06fc..0b6e703 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_ListUserParams_Response.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_ListUserParams_Response.java
@@ -20,7 +20,7 @@ import java.util.List;
* "param": "feed:lastSeenGlobal",
* "time_ms": 1736000000123,
* "value": "105",
- * "device_key": "base64-32",
+ * "client_key": "base64-32",
* "signature": "base64-64"
* },
* ...
@@ -44,7 +44,7 @@ public class Net_ListUserParams_Response extends Net_Response {
private String param;
private Long time_ms;
private String value;
- private String device_key;
+ private String client_key;
private String signature;
public String getLogin() { return login; }
@@ -59,8 +59,8 @@ public class Net_ListUserParams_Response extends Net_Response {
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
+ public String getClient_key() { return client_key; }
+ public void setClient_key(String client_key) { this.client_key = client_key; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_UpsertUserParam_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_UpsertUserParam_Request.java
index ed1c7ff..4d8c159 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_UpsertUserParam_Request.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/userParams/entyties/Net_UpsertUserParam_Request.java
@@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
* "param": "feed:lastSeenGlobal",
* "time_ms": 1736000000123,
* "value": "105",
- * "device_key": "base64-ed25519-public-key-32",
+ * "client_key": "base64-ed25519-public-key-32",
* "signature": "base64-ed25519-signature-64"
* }
* }
@@ -30,7 +30,7 @@ public class Net_UpsertUserParam_Request extends Net_Request {
private Long time_ms;
private String value;
- private String device_key;
+ private String client_key;
private String signature;
public String getLogin() { return login; }
@@ -45,8 +45,8 @@ public class Net_UpsertUserParam_Request extends Net_Request {
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
- public String getDevice_key() { return device_key; }
- public void setDevice_key(String device_key) { this.device_key = device_key; }
+ public String getClient_key() { return client_key; }
+ public void setClient_key(String client_key) { this.client_key = client_key; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_SendDirectMessage_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_SendDirectMessage_Handler.java
index 435d915..353f335 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_SendDirectMessage_Handler.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_SendDirectMessage_Handler.java
@@ -62,9 +62,9 @@ public class Net_SendDirectMessage_Handler implements JsonMessageHandler {
byte[] publicKey32;
try {
- publicKey32 = Ed25519Util.keyFromBase64(fromUser.getDeviceKey());
+ publicKey32 = Ed25519Util.keyFromBase64(fromUser.getClientKey());
} catch (Exception e) {
- return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный deviceKey отправителя");
+ return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный clientKey отправителя");
}
if (!Ed25519Util.verify(packet.signedBody, packet.signature64, publicKey32)) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку");
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/SignedMessagesCore.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/SignedMessagesCore.java
index 4e2bf93..0030ba7 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/SignedMessagesCore.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/SignedMessagesCore.java
@@ -44,7 +44,7 @@ final class SignedMessagesCore {
if (from == null || to == null) {
throw new IllegalArgumentException("USER_NOT_FOUND");
}
- byte[] pubKey32 = Ed25519Util.keyFromBase64(from.getDeviceKey());
+ byte[] pubKey32 = Ed25519Util.keyFromBase64(from.getClientKey());
if (!Ed25519Util.verify(block.signedBody, block.signature64, pubKey32)) {
throw new IllegalArgumentException("BAD_SIGNATURE");
}
diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/utils/AuthSignatures.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/utils/AuthSignatures.java
index be3c997..539ab1e 100644
--- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/utils/AuthSignatures.java
+++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/utils/AuthSignatures.java
@@ -31,7 +31,7 @@
// }
//
// /**
-// * Проверка подписи CreateAuthSession(v2) по deviceKey пользователя.
+// * Проверка подписи CreateAuthSession(v2) по clientKey пользователя.
// * Подпись проверяется над preimageCreateAuthSession(...).
// */
// public static boolean verifyCreateAuthSessionSignature(
@@ -42,8 +42,8 @@
// String signatureB64
// ) throws IllegalArgumentException {
//
-// // user.getDeviceKey() — base64 публичного ключа (32 байта)
-// byte[] publicKey32 = decodeBase64Any(user.getDeviceKey());
+// // user.getClientKey() — base64 публичного ключа (32 байта)
+// byte[] publicKey32 = decodeBase64Any(user.getClientKey());
// byte[] signature64 = decodeBase64Any(signatureB64);
//
// byte[] preimage = preimageCreateAuthSession(login, timeMs, authNonce);
diff --git a/SHiNE-server/src/main/all_files.txt b/SHiNE-server/src/main/all_files.txt
deleted file mode 100644
index 83d0c14..0000000
--- a/SHiNE-server/src/main/all_files.txt
+++ /dev/null
@@ -1,552 +0,0 @@
-package server.logic;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.ws_protocol.binary.handlers.*;
-import server.logic.ws_protocol.WireCodes;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.Map;
-
-/**
- * Обработчик входящих сообщение на сервер.
- * По коду сообщения (первые 4 байта сообщения) находи нужный хэндлер и передаёт в него сообщение
- * Получает и возвращает ответ от хэндлера
- */
-public final class InboundMessageProcessor {
- private static final Logger log = LoggerFactory.getLogger(InboundMessageProcessor.class);
-
- private static final Map HANDLERS = Map.of(
-// WireCodes.Op.PING, new PingHandler()
-// WireCodes.Op.ADD_BLOCK, new AddBlockHandler(),
-// WireCodes.Op.GET_BLOCKCHAIN,new GetBlockchainHandler()
-// WireCodes.Op.SEARCH_USERS, new SearchUsersHandler(),
-// WireCodes.Op.GET_LAST_BLOCK_INFO,new GetLastBlockInfoHandler()
-
- );
-
- private InboundMessageProcessor() {}
-
- public static byte[] process(byte[] msg) {
- if (msg == null || msg.length < 4)
- return intTo4Bytes(WireCodes.Status.BAD_REQUEST);
-
- int op = first4ToInt(msg);
- MessageHandler h = HANDLERS.get(op);
- if (h == null) {
- log.warn("Неизвестная операция: {}", op);
- return intTo4Bytes(WireCodes.Status.BAD_REQUEST);
- }
-
- try {
- return h.handle(msg);
- } catch (Exception e) {
- log.error("Ошибка при обработке операции {}", op, e);
- return intTo4Bytes(WireCodes.Status.INTERNAL_ERROR);
- }
- }
-
- private static int first4ToInt(byte[] msg) {
- return ByteBuffer.wrap(msg, 0, 4)
- .order(ByteOrder.BIG_ENDIAN)
- .getInt();
- }
-
- public static byte[] intTo4Bytes(int code) {
- return ByteBuffer.allocate(4)
- .order(ByteOrder.BIG_ENDIAN)
- .putInt(code)
- .array();
- }
-
-
-
-}
-
-
-package server.logic.ws_protocol.binary.handlers;
-
-/**
- * Общий интерфейс для всех обработчиков входящих сообщений.
- */
-public interface MessageHandler {
- /**
- * Обработать входящее сообщение и вернуть бинарный ответ.
- */
- byte[] handle(byte[] msg);
-}
-
-package server.ws;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import shine.db.dao.BlockchainStateDAO;
-import shine.db.entities.BlockchainStateEntry;
-import utils.files.FileStoreUtil;
-import shine.log.BlockchainAdminNotifier;
-
-import java.io.IOException;
-import java.nio.file.*;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * ===============================================================
- * BlockchainTmpRecoveryOnStartup — восстановление консистентности
- * blockchain файлов при старте сервера.
- *
- * Сценарий проблемы:
- * - при добавлении блока сначала пишется .tmp_bch
- * - потом коммитится БД (state.fileSizeBytes)
- * - потом tmp переименовывается поверх .bch (атомарно, если возможно)
- *
- * Если сервер упал в середине, может остаться tmp:
- * - tmp есть, а основной .bch остался старым
- * - tmp есть, а основной .bch уже удалили/заменить не успели
- * - tmp есть, а БД успела/не успела обновиться
- *
- * Этот класс при старте:
- * - ищет все *.tmp_bch в data/
- * - сравнивает размеры:
- * - tmp
- * - main (если есть)
- * - state.fileSizeBytes (если есть)
- *
- * Правила:
- *
- * A) state есть:
- * - если stateSize == mainSize => tmp удаляем
- * - если stateSize == tmpSize => tmp ставим на место main (atomicReplaceBlockchainFile)
- * - иначе => КРИТИЧЕСКАЯ ОШИБКА: сервер останавливаем + уведомление администратору
- *
- * B) state НЕТ:
- * - если main НЕТ и tmp ЕСТЬ => tmp удаляем (мусор после падения/неуспешной транзакции)
- * - если main ЕСТЬ и tmp ЕСТЬ => КРИТИЧЕСКАЯ ОШИБКА: уведомление администратору + стоп сервера
- *
- * Логирование:
- * - обо всех восстановленных/удалённых tmp пишем в лог
- * - если tmp-файлов нет — тоже пишем в лог
- * ===============================================================
- */
-public final class BlockchainTmpRecoveryOnStartup {
-
- private static final Logger log = LoggerFactory.getLogger(BlockchainTmpRecoveryOnStartup.class);
-
- private BlockchainTmpRecoveryOnStartup() {}
-
- /**
- * Запуск восстановления.
- * Если обнаружена ситуация, когда размеры не совпали и сервер сам не может чинить — бросаем исключение.
- */
- public static void runRecoveryOrThrow() {
- FileStoreUtil fs = FileStoreUtil.getInstance();
- BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
-
- Path dataDir = Paths.get(FileStoreUtil.DATA_DIR_NAME);
- ensureDirExists(dataDir);
-
- List tmpFiles = listTmpFiles(dataDir);
-
- if (tmpFiles.isEmpty()) {
- log.info("🟢 BlockchainTmpRecovery: временных *.tmp_bch файлов не найдено — восстановление не требуется.");
- return;
- }
-
- log.warn("🟡 BlockchainTmpRecovery: найдено временных файлов: {}", tmpFiles.size());
-
- for (Path tmpPath : tmpFiles) {
- String fileName = tmpPath.getFileName().toString();
- String blockchainName = extractBlockchainNameFromTmp(fileName);
-
- if (blockchainName == null || blockchainName.isBlank()) {
- // странное имя — не трогаем автоматически, но это уже повод дернуть админа
- BlockchainAdminNotifier.critical(
- "НАЙДЕН TMP-ФАЙЛ С НЕОЖИДАННЫМ ИМЕНЕМ: " + fileName + " (не могу определить blockchainName).",
- null
- );
- throw new IllegalStateException("Bad tmp file name: " + fileName);
- }
-
- Path mainPath = dataDir.resolve(fs.buildBlockchainFileName(blockchainName));
-
- long tmpSize = safeSize(tmpPath);
- boolean mainExists = Files.exists(mainPath);
- long mainSize = mainExists ? safeSize(mainPath) : -1L;
-
- BlockchainStateEntry st = null;
- try {
- st = stateDAO.getByBlockchainName(blockchainName);
- } catch (SQLException e) {
- BlockchainAdminNotifier.critical(
- "ОШИБКА БД ПРИ ВОССТАНОВЛЕНИИ TMP: blockchainName=" + blockchainName + " (сервер остановлен).",
- e
- );
- throw new IllegalStateException("DB error during tmp recovery for " + blockchainName, e);
- }
-
- // ============================================================
- // CASE B) state НЕТ
- // ============================================================
- if (st == null) {
-
- if (!mainExists) {
- // НЕТ state, НЕТ main, есть tmp => удаляем tmp
- log.warn("🟠 BlockchainTmpRecovery: state отсутствует и main отсутствует, но tmp найден => удаляем tmp. blockchainName={}, tmpSize={}",
- blockchainName, tmpSize);
- safeDelete(tmpPath);
- continue;
- }
-
- // НЕТ state, но main есть и tmp есть => это уже подозрительно
- BlockchainAdminNotifier.critical(
- "НЕСОГЛАСОВАННОСТЬ: ЕСТЬ main И tmp, НО НЕТ state В БД. " +
- "blockchainName=" + blockchainName +
- ", mainSize=" + mainSize +
- ", tmpSize=" + tmpSize +
- ". СЕРВЕР ОСТАНОВЛЕН. " +
- "ПОДОЗРЕНИЕ: файлы могли быть изменены вне сервера.",
- null
- );
- throw new IllegalStateException("State missing but both main and tmp exist for " + blockchainName);
- }
-
- // ============================================================
- // CASE A) state ЕСТЬ
- // ============================================================
- long stateSize = st.getFileSizeBytes();
-
- // 1) stateSize == mainSize => tmp мусор
- if (mainExists && mainSize == stateSize) {
- log.info("🟢 BlockchainTmpRecovery: stateSize совпадает с main => tmp удаляем. blockchainName={}, stateSize={}, mainSize={}, tmpSize={}",
- blockchainName, stateSize, mainSize, tmpSize);
- safeDelete(tmpPath);
- continue;
- }
-
- // 2) stateSize == tmpSize => tmp это актуальная версия, ставим на место main
- if (tmpSize == stateSize) {
- log.warn("🟡 BlockchainTmpRecovery: stateSize совпадает с tmp => восстанавливаем main из tmp. blockchainName={}, stateSize={}, mainSize={}, tmpSize={}",
- blockchainName, stateSize, mainSize, tmpSize);
-
- try {
- // метод уже есть и делает move tmp->main с попыткой ATOMIC_MOVE
- fs.atomicReplaceBlockchainFile(blockchainName);
-
- // после move tmp должен исчезнуть сам (перемещён)
- log.info("✅ BlockchainTmpRecovery: восстановление выполнено. blockchainName={}, newMainSize={}",
- blockchainName, safeSize(mainPath));
-
- } catch (Exception e) {
- BlockchainAdminNotifier.critical(
- "НЕ УДАЛОСЬ ВОССТАНОВИТЬ main ИЗ tmp (move failed). " +
- "blockchainName=" + blockchainName +
- ", stateSize=" + stateSize +
- ", mainSize=" + mainSize +
- ", tmpSize=" + tmpSize +
- ". СЕРВЕР ОСТАНОВЛЕН.",
- e
- );
- throw new IllegalStateException("Cannot replace main from tmp for " + blockchainName, e);
- }
- continue;
- }
-
- // 3) НИЧЕГО НЕ СОВПАЛО => критическая ситуация
- BlockchainAdminNotifier.critical(
- "ФАТАЛЬНАЯ НЕСОГЛАСОВАННОСТЬ BLOCKCHAIN ФАЙЛОВ. " +
- "blockchainName=" + blockchainName +
- ", stateSize=" + stateSize +
- ", mainExists=" + mainExists +
- ", mainSize=" + mainSize +
- ", tmpSize=" + tmpSize +
- ". СЕРВЕР ОСТАНОВЛЕН. " +
- "ТУТ НУЖНО УВЕДОМЛЕНИЕ АДМИНИСТРАТОРУ: возможно файлы изменены вручную/другой программой.",
- null
- );
- throw new IllegalStateException("Blockchain files mismatch for " + blockchainName);
- }
-
- log.info("✅ BlockchainTmpRecovery: обработка tmp-файлов завершена.");
- }
-
- /* ===================================================================== */
- /* =============================== Helpers ============================== */
- /* ===================================================================== */
-
- private static void ensureDirExists(Path dir) {
- try {
- if (!Files.exists(dir)) {
- Files.createDirectories(dir);
- }
- } catch (IOException e) {
- throw new IllegalStateException("Cannot create data dir: " + dir, e);
- }
- }
-
- private static List listTmpFiles(Path dataDir) {
- List out = new ArrayList<>();
- try (DirectoryStream ds = Files.newDirectoryStream(dataDir, "*" + FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION)) {
- for (Path p : ds) {
- if (Files.isRegularFile(p)) out.add(p);
- }
- } catch (IOException e) {
- throw new IllegalStateException("Cannot list tmp files in: " + dataDir, e);
- }
- return out;
- }
-
- /**
- * Из "anya0001.tmp_bch" -> "anya0001"
- */
- private static String extractBlockchainNameFromTmp(String tmpFileName) {
- if (tmpFileName == null) return null;
- if (!tmpFileName.endsWith(FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION)) return null;
-
- String base = tmpFileName.substring(0, tmpFileName.length() - FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION.length());
-
- // базовая защита: не допускаем слэши/.. даже если кто-то подложил файл
- if (base.isBlank()) return null;
- if (base.contains("/") || base.contains("\\") || base.contains("..")) return null;
-
- return base;
- }
-
- private static long safeSize(Path p) {
- try {
- return Files.size(p);
- } catch (IOException e) {
- throw new IllegalStateException("Cannot read file size: " + p, e);
- }
- }
-
- private static void safeDelete(Path p) {
- try {
- Files.deleteIfExists(p);
- } catch (IOException e) {
- throw new IllegalStateException("Cannot delete file: " + p, e);
- }
- }
-}
-package server.ws;
-
-import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.WriteCallback;
-import org.eclipse.jetty.websocket.api.annotations.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import server.logic.InboundMessageProcessor;
-import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
-import server.logic.ws_protocol.JSON.ConnectionContext;
-import server.logic.ws_protocol.JSON.JsonInboundProcessor;
-
-import java.nio.ByteBuffer;
-import java.util.concurrent.CompletableFuture;
-
-@WebSocket
-public class BlockchainWsEndpoint {
- private static final Logger log = LoggerFactory.getLogger(BlockchainWsEndpoint.class);
-
- private Session session;
-
- /** Контекст для текущего WebSocket-соединения. */
- private final ConnectionContext connectionContext = new ConnectionContext();
-
- @OnWebSocketConnect
- public void onConnect(Session session) {
- this.session = session;
- // Привязываем WebSocket-сессию к ConnectionContext
- connectionContext.setWsSession(session);
- log.info("WS connected: {}", session.getRemoteAddress());
- }
-
- @OnWebSocketMessage
- public void onBinary(byte[] payload, int offset, int length) {
- byte[] msg = new byte[length];
- System.arraycopy(payload, offset, msg, 0, length);
-
- // Асинхронно обрабатываем входящее бинарное сообщение
- CompletableFuture
- .supplyAsync(() -> InboundMessageProcessor.process(msg))
- .thenAccept(resp -> {
- if (resp != null && session != null && session.isOpen()) {
- session.getRemote().sendBytes(ByteBuffer.wrap(resp), new WriteCallback() {
- @Override
- public void writeFailed(Throwable x) {
- log.warn("Failed to send response", x);
- }
-
- @Override
- public void writeSuccess() {
- log.debug("Response sent successfully");
- }
- });
- }
- })
- .exceptionally(ex -> {
- log.error("Processing failed", ex);
- trySendCode(500);
- return null;
- });
- }
-
- private void trySendCode(int code) {
- if (session != null && session.isOpen()) {
- byte[] resp = InboundMessageProcessor.intTo4Bytes(code);
- session.getRemote().sendBytes(ByteBuffer.wrap(resp), new WriteCallback() {
- @Override
- public void writeFailed(Throwable x) {
- log.warn("Failed to send error code", x);
- }
-
- @Override
- public void writeSuccess() {
- log.debug("Error code {} sent", code);
- }
- });
- }
- }
-
- @OnWebSocketClose
- public void onClose(int statusCode, String reason) {
- log.info("WS closed: {} {}", statusCode, reason);
- // Удаляем это подключение из реестра активных соединений
- ActiveConnectionsRegistry.getInstance().remove(connectionContext);
- // На всякий случай очищаем контекст
- connectionContext.reset();
- }
-
- @OnWebSocketError
- public void onError(Throwable cause) {
- log.error("WS error", cause);
- }
-
- // Обработка текстовых JSON-запросов
- @OnWebSocketMessage
- public void onText(String message) {
- log.info("📥 Получено TEXT-сообщение от клиента: {}", message);
-
- CompletableFuture
- .supplyAsync(() -> JsonInboundProcessor.processJson(message, connectionContext))
- .thenAccept(respJson -> {
- if (respJson != null && session != null && session.isOpen()) {
-
- log.info("📤 Отправляем ответ клиенту: {}", respJson);
-
- session.getRemote().sendString(respJson, new WriteCallback() {
- @Override
- public void writeFailed(Throwable x) {
- log.warn("⚠️ Не удалось отправить JSON-ответ клиенту: {}", x.toString());
- }
-
- @Override
- public void writeSuccess() {
- log.debug("✔ JSON-ответ успешно отправлен");
- }
- });
- }
- })
- .exceptionally(ex -> {
- log.error("❌ Ошибка при обработке JSON-сообщения", ex);
- trySendJsonError();
- return null;
- });
- }
-
- private void trySendJsonError() {
- if (session != null && session.isOpen()) {
- String resp = "{\"op\":null,\"requestId\":null,\"status\":500,"
- + "\"payload\":{\"code\":\"INTERNAL_ERROR\",\"message\":\"Ошибка сервера\"}}";
-
- log.info("📤 Отправляем клиенту ошибку JSON: {}", resp);
-
- session.getRemote().sendString(resp, new WriteCallback() {
- @Override
- public void writeFailed(Throwable x) {
- log.warn("⚠️ Не удалось отправить JSON-ответ клиенту: {}", x.toString());
- }
-
- @Override
- public void writeSuccess() {
- log.debug("✔ JSON-ошибка успешно отправлена");
- }
- });
- }
- }
-}
-
-package server.ws;
-
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import utils.config.AppConfig;
-
-import java.time.Duration;
-
-/**
- * WsServer — поднимает Jetty WS на /ws.
- *
- * ВАЖНО:
- * - перед стартом сервера выполняем recovery tmp-блокчейнов.
- * - если обнаружена несогласованность, которую сервер сам чинить не может —
- * recovery бросает исключение и сервер не стартует.
- */
-public final class WsServer {
-
- private static final Logger log = LoggerFactory.getLogger(WsServer.class);
-
- public static void main(String[] args) throws Exception {
-
- // ============================================================
- // 0) Восстановление консистентности blockchain файлов
- // ============================================================
- try {
- BlockchainTmpRecoveryOnStartup.runRecoveryOrThrow();
- } catch (Exception e) {
- // Уже должно быть “большое” уведомление через BlockchainAdminNotifier,
- // но на всякий случай логируем ещё раз.
- log.error("❌ Сервер НЕ будет запущен: критическая ошибка восстановления blockchain tmp-файлов.", e);
- throw e; // останавливаем запуск
- }
-
- // ============================================================
- // 1) Настройки порта
- // ============================================================
- AppConfig config = AppConfig.getInstance();
- int port = 7070;
- try {
- String portStr = config.getParam("server.port");
- if (portStr != null && !portStr.isBlank()) {
- port = Integer.parseInt(portStr.trim());
- }
- } catch (Exception e) {
- log.info("Не удалось прочитать параметр server.port, используем порт по умолчанию {}", port);
- }
-
- // ============================================================
- // 2) Запуск Jetty WS
- // ============================================================
- Server server = new Server(port);
-
- ServletContextHandler context = new ServletContextHandler();
- context.setContextPath("/");
- server.setHandler(context);
-
- // Инициализация контейнера WebSocket
- JettyWebSocketServletContainerInitializer.configure(context, (servletContext, wsContainer) -> {
- // Таймаут простоя соединения (Jetty 11 синтаксис)
- wsContainer.setIdleTimeout(Duration.ofMinutes(5));
-
- // Маппинг эндпоинта
- wsContainer.addMapping("/ws", (req, resp) -> new BlockchainWsEndpoint());
- });
-
- server.start();
- log.info("✅ WS сервер запущен на ws://localhost:{}/ws", port);
- server.join();
- }
-}
diff --git a/SHiNE-server/src/main/concat_to_file.sh b/SHiNE-server/src/main/concat_to_file.sh
deleted file mode 100755
index f6db1f1..0000000
--- a/SHiNE-server/src/main/concat_to_file.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/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
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Все .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/src/main/concat_to_file2.sh b/SHiNE-server/src/main/concat_to_file2.sh
deleted file mode 100755
index dc5f5d1..0000000
--- a/SHiNE-server/src/main/concat_to_file2.sh
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-OUTFILE="all_files.txt"
-SKIPFILE="skip.txt"
-
-# очищаем или создаём файл
-: > "$OUTFILE"
-
-# читаем список исключённых имён (без расширения) в массив
-if [[ -f "$SKIPFILE" ]]; then
- mapfile -t SKIP_LIST < "$SKIPFILE"
-else
- SKIP_LIST=()
-fi
-
-find . -type f -name "*.java" | sort | while read -r f; do
- fname=$(basename "$f" .java) # имя файла без расширения
-
- # проверяем, есть ли имя в списке исключений
- skip=false
- for skipf in "${SKIP_LIST[@]}"; do
- if [[ "$fname" == "$skipf" ]]; then
- skip=true
- break
- fi
- done
-
- if [[ "$skip" == true ]]; then
- echo "Пропускаем $f"
- continue
- fi
-
- cat "$f" >> "$OUTFILE"
- echo >> "$OUTFILE" # пустая строка-разделитель
-done
-
-echo "Готово! Все .java файлы собраны в $OUTFILE (кроме исключённых из $SKIPFILE)"
diff --git a/SHiNE-server/src/main/запросы.sh b/SHiNE-server/src/main/запросы.sh
deleted file mode 100644
index 02c8a81..0000000
--- a/SHiNE-server/src/main/запросы.sh
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-# OUTFILE:
-# - если пустая строка ("") -> в файл НЕ пишем, только в буфер
-# - если не пустая -> пишем в файл + (если есть wl-copy) копируем в буфер
-OUTFILE="all_files.txt"
-# OUTFILE=""
-
-# === НАСТРОЙКА: перечисляй тут пути (каталоги и/или конкретные файлы) ===
-# - Если путь указывает на ФАЙЛ: берём его ВСЕГДА, даже если это не .java
-# - Если путь указывает на КАТАЛОГ: рекурсивно берём только *.java внутри
-# - Пустые строки игнорируются
-TARGETS=(
- #"./src/main/java"
-# "./server"
-# /home/ai/work/SHiNE/SHiNE-server/shine-server-blockchain
- "/home/ai/work/SHiNE/SHiNE-server/shine-server-blockchain"
- "/home/ai/work/SHiNE/SHiNE-server/shine-server-db"
-)
-
-RED=$'\033[0;31m'
-RESET=$'\033[0m'
-
-warn_red() {
- echo "${RED}WARN:${RESET} $*" >&2
-}
-
-# временные файлы
-TMP_LIST="$(mktemp)"
-TMP_OUT="$(mktemp)"
-trap 'rm -f "$TMP_LIST" "$TMP_OUT"' EXIT
-
-# собрать пути
-for path in "${TARGETS[@]}"; do
- path="$(printf '%s' "$path" | sed -e 's/^[[:space:]]\+//' -e 's/[[:space:]]\+$//')"
- [[ -z "$path" ]] && continue
-
- if [[ -f "$path" ]]; then
- printf '%s\n' "$path" >> "$TMP_LIST"
- elif [[ -d "$path" ]]; then
- find "$path" -type f -name "*.java" >> "$TMP_LIST"
- else
- warn_red "Не найдено (пропускаю): $path"
- fi
-done
-
-# склеиваем в TMP_OUT
-sort -u "$TMP_LIST" | while IFS= read -r f; do
- if [[ ! -f "$f" ]]; then
- warn_red "Файл исчез (пропускаю): $f"
- continue
- fi
- cat "$f" >> "$TMP_OUT"
- echo >> "$TMP_OUT"
-done
-
-# если OUTFILE не пуст — пишем файл
-if [[ -n "${OUTFILE:-}" ]]; then
- : > "$OUTFILE"
- cat "$TMP_OUT" > "$OUTFILE"
-fi
-
-# копирование в буфер (Wayland), если доступно
-if command -v wl-copy >/dev/null 2>&1; then
- wl-copy < "$TMP_OUT"
-else
- warn_red "wl-copy не найден — в буфер не скопировано."
-fi
-
-echo "Готово!"
-if [[ -n "${OUTFILE:-}" ]]; then
- echo "Все файлы собраны в $OUTFILE"
-else
- echo "OUTFILE пуст — в файл не писали, только буфер (если wl-copy доступен)"
-fi
diff --git a/SHiNE-server/src/test/addblocks.sh b/SHiNE-server/src/test/addblocks.sh
deleted file mode 100755
index 5f8f10c..0000000
--- a/SHiNE-server/src/test/addblocks.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-OUTFILE="all_files.txt"
-
-# === Список файлов (ТОЛЬКО имена без расширений) ===
-# пример: Main значит Main.java, Utils значит Utils.java
-NAMES=(
- "IT_04_UserParams_NoAuth"
- "AddBlockSender"
- "ChainState"
- "JsonBuilders"
-)
-
-# очищаем или создаём файл
-: > "$OUTFILE"
-
-# Быстрый фильтр: сделаем хеш-таблицу из имён (ассоц. массив)
-declare -A WANT=()
-for name in "${NAMES[@]}"; do
- WANT["$name"]=1
-done
-
-# собрать только нужные *.java по базовому имени
-find . -type f -name "*.java" | sort | while read -r f; do
- base="$(basename "$f" .java)"
- if [[ -n "${WANT[$base]+x}" ]]; then
- cat "$f" >> "$OUTFILE"
- echo >> "$OUTFILE" # пустая строка-разделитель
- fi
-done
-
-# скопировать весь файл в буфер обмена (Wayland)
-wl-copy < "$OUTFILE"
-
-echo "Готово!"
-echo "Выбрано имён: ${#NAMES[@]}"
-echo "Все нужные .java файлы собраны в $OUTFILE"
-echo "Содержимое скопировано в буфер обмена (Wayland)"
diff --git a/SHiNE-server/src/test/all_files.txt b/SHiNE-server/src/test/all_files.txt
deleted file mode 100644
index ebc0734..0000000
--- a/SHiNE-server/src/test/all_files.txt
+++ /dev/null
@@ -1,2951 +0,0 @@
-package test.it.blockchain;
-
-import blockchain.BchBlockEntry;
-import blockchain.body.*;
-import test.it.utils.TestConfig;
-import test.it.utils.TestIds;
-import test.it.utils.json.JsonParsers;
-import test.it.utils.log.TestLog;
-import test.it.utils.ws.WsSession;
-
-import java.time.Duration;
-import java.util.Base64;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
-/**
- * AddBlockSender — под новый формат BchBlockEntry (Frame v0):
- * - blockBytes = preimage + sigMarker(2) + signature64
- * - preimage начинается с frameCode(2) = 0x0000
- * - hash32 вычисляется как sha256(preimage)
- * - signature = Ed25519.sign(hash32)
- *
- * ВАЖНО:
- * - Линии по ТЗ ведём на стороне сервера/БД (триггеры), а в тестах считаем локально.
- */
-public final class AddBlockSender {
-
- private static final String ZERO64 = "0".repeat(64);
-
- private final WsSession ws;
- private final ChainState state;
-
- private final String login;
- private final String blockchainName;
- private final byte[] loginPrivKey;
-
- public AddBlockSender(WsSession ws, ChainState state, String login, String blockchainName, byte[] loginPrivKey) {
- this.ws = ws;
- this.state = state;
- this.login = login;
- this.blockchainName = blockchainName;
- this.loginPrivKey = (loginPrivKey == null ? null : loginPrivKey.clone());
- if (this.ws == null) throw new IllegalArgumentException("ws == null");
- if (this.state == null) throw new IllegalArgumentException("state == null");
- if (this.loginPrivKey == null) throw new IllegalArgumentException("loginPrivKey == null");
- }
-
- public ChainState state() { return state; }
-
- public void send(BodyRecord body, Duration timeout) {
- if (body == null) throw new IllegalArgumentException("body == null");
-
- body.check();
-
- boolean isHeader = (body instanceof HeaderBody);
-
- if (isHeader) {
- if (state.lastBlockNumber() != -1) {
- throw new IllegalStateException("HEADER должен быть первым: lastBlockNumber уже " + state.lastBlockNumber());
- }
- } else {
- if (!state.hasHeader()) {
- throw new IllegalStateException("Нельзя слать блоки до HEADER (нет headerHash32)");
- }
- }
-
- int blockNumber = state.nextBlockNumber();
- byte[] prevHash32 = state.prevHash32ForNext();
- long tsSec = System.currentTimeMillis() / 1000L;
-
- short type = typeOf(body);
- short subType = subTypeOf(body);
- short version = versionOf(body);
-
- byte[] bodyBytes = body.toBytes();
-
- // ВАЖНО: preimage должен быть БАЙТ-В-БАЙТ таким же, как в BchBlockEntry
- byte[] preimage = buildPreimage(prevHash32, blockNumber, tsSec, type, subType, version, bodyBytes);
-
- byte[] hash32 = blockchain.BchCryptoVerifier.sha256(preimage);
- byte[] signature64 = utils.crypto.Ed25519Util.sign(hash32, loginPrivKey);
-
- BchBlockEntry entry = new BchBlockEntry(
- prevHash32,
- blockNumber,
- tsSec,
- type,
- subType,
- version,
- bodyBytes,
- signature64
- );
-
- String prevHashHexForReq = (blockNumber == 0) ? ZERO64 : state.lastBlockHashHex();
-
- String reqJson = buildAddBlockJson(blockchainName, blockNumber, prevHashHexForReq, base64(entry.toBytes()));
- String op = "AddBlock(user=" + login + ", block=" + blockNumber + ", type=" + (type & 0xFFFF) + ", sub=" + (subType & 0xFFFF) + ")";
-
- String resp = ws.call(op, reqJson, timeout);
-
- assert200(op, resp);
-
- String serverLastHash = JsonMini.extractPayloadString(resp, "serverLastBlockHash");
- if (serverLastHash == null) {
- serverLastHash = JsonMini.extractPayloadString(resp, "serverLastGlobalHash");
- }
-
- assertNotNull(serverLastHash, op + ": payload.serverLastBlockHash must not be null");
- assertEquals(64, serverLastHash.trim().length(), op + ": serverLastBlockHash must be 64 hex chars");
-
- String localHashHex = bytesToHex64(entry.getHash32());
-
- if (TestConfig.DEBUG()) {
- TestLog.info(op + ": localHash=" + localHashHex);
- TestLog.info(op + ": serverLastBlockHash=" + serverLastHash);
- }
-
- assertEquals(localHashHex, serverLastHash, op + ": serverLastBlockHash must match local hash");
-
- state.applyAppendedBlock(blockNumber, entry.getHash32(), isHeader, type, body);
-
- if (TestConfig.DEBUG()) TestLog.info(op + ": state updated");
- }
-
- // ---------- request JSON ----------
-
- private static String buildAddBlockJson(String blockchainName, int blockNumber, String prevBlockHashHex, String blockBytesB64) {
- String requestId = TestIds.next("addblock");
- return """
- {
- "op": "AddBlock",
- "requestId": "%s",
- "payload": {
- "blockchainName": "%s",
- "blockNumber": %d,
- "prevBlockHash": "%s",
- "blockBytesB64": "%s"
- }
- }
- """.formatted(requestId, blockchainName, blockNumber, prevBlockHashHex, blockBytesB64);
- }
-
- private static void assert200(String op, String resp) {
- int st = JsonParsers.status(resp);
- assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
- TestLog.ok(op + ": status=200");
- }
-
- private static String base64(byte[] bytes) {
- return Base64.getEncoder().encodeToString(bytes);
- }
-
- private static String bytesToHex64(byte[] b32) {
- char[] out = new char[64];
- final char[] HEX = "0123456789abcdef".toCharArray();
- for (int i = 0; i < 32; i++) {
- int v = b32[i] & 0xFF;
- out[i * 2] = HEX[v >>> 4];
- out[i * 2 + 1] = HEX[v & 0x0F];
- }
- return new String(out);
- }
-
- // ---------- header extraction from body ----------
-
- private static short typeOf(BodyRecord body) {
- if (body instanceof HeaderBody) return HeaderBody.TYPE;
- if (body instanceof CreateChannelBody) return CreateChannelBody.TYPE;
- if (body instanceof TextBody) return TextBody.TYPE;
- if (body instanceof ReactionBody) return ReactionBody.TYPE;
- if (body instanceof ConnectionBody) return ConnectionBody.TYPE;
- if (body instanceof UserParamBody) return UserParamBody.TYPE;
- throw new IllegalArgumentException("Unknown body class: " + body.getClass());
- }
-
- private static short subTypeOf(BodyRecord body) {
- if (body instanceof HeaderBody hb) return hb.subType;
- if (body instanceof CreateChannelBody cb) return cb.subType;
- if (body instanceof TextBody tb) return tb.subType;
- if (body instanceof ReactionBody rb) return rb.subType;
- if (body instanceof ConnectionBody cb) return cb.subType;
- if (body instanceof UserParamBody ub) return ub.subType;
- throw new IllegalArgumentException("Unknown body class: " + body.getClass());
- }
-
- private static short versionOf(BodyRecord body) {
- if (body instanceof HeaderBody hb) return hb.version;
- if (body instanceof CreateChannelBody cb) return cb.version;
- if (body instanceof TextBody tb) return tb.version;
- if (body instanceof ReactionBody rb) return rb.version;
- if (body instanceof ConnectionBody cb) return cb.version;
- if (body instanceof UserParamBody ub) return ub.version;
- throw new IllegalArgumentException("Unknown body class: " + body.getClass());
- }
-
- // ---------- preimage builder (строго по BchBlockEntry Frame v0) ----------
-
- private static byte[] buildPreimage(byte[] prevHash32,
- int blockNumber,
- long tsSec,
- short type,
- short subType,
- short version,
- byte[] bodyBytes) {
-
- if (prevHash32 == null || prevHash32.length != 32) {
- throw new IllegalArgumentException("prevHash32 must be 32 bytes");
- }
-
- int bodyLen = (bodyBytes == null ? 0 : bodyBytes.length);
- int blockSize = BchBlockEntry.PREIMAGE_HEADER_SIZE + bodyLen;
-
- java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(blockSize).order(java.nio.ByteOrder.BIG_ENDIAN);
-
- // [2] frameCode (v0)
- bb.putShort((short) (BchBlockEntry.FRAME_CODE_V0 & 0xFFFF));
-
- // [32] prevHash32
- bb.put(prevHash32);
-
- // [4] blockSize (preimage size)
- bb.putInt(blockSize);
-
- // [4] blockNumber
- bb.putInt(blockNumber);
-
- // [8] timestamp
- bb.putLong(tsSec);
-
- // [2][2][2] type/subType/version
- bb.putShort(type);
- bb.putShort(subType);
- bb.putShort(version);
-
- // [N] bodyBytes
- if (bodyBytes != null) bb.put(bodyBytes);
-
- return bb.array();
- }
-}
-package test.it.blockchain;
-
-import blockchain.body.BodyRecord;
-import blockchain.body.BodyHasLine;
-import blockchain.body.CreateChannelBody;
-import blockchain.body.TextBody;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * ChainState — состояние глобальной цепочки + состояние линий.
- *
- * Глобальная цепочка:
- * - lastBlockNumber / lastBlockHashHex
- * - map blockNumber -> hash32
- *
- * Линии:
- * - TECH (type=0): только CREATE_CHANNEL (hasLine), root = HEADER
- * - TEXT (type=1): линии каналов, root = HEADER (канал "0") или CREATE_CHANNEL (канал "X")
- * - CONNECTION (type=3): одна линия
- * - USER_PARAM (type=4): одна линия
- *
- * ВАЖНО:
- * - prevLineNumber — это GLOBAL blockNumber предыдущего блока линии.
- * - thisLineNumber — внутренний номер линии (для постов: 0,1,2...; для тех-линии: 1,2,3...)
- * - lineCode — код линии:
- * * 0 для канала "0" и для "простых" линий (connection/user_param/tech)
- * * для каналов !=0: lineCode = blockNumber "заглавия" канала (CREATE_CHANNEL)
- */
-public final class ChainState {
-
- public static final short TYPE_TECH = 0; // header/create_channel
- public static final short TYPE_TEXT = 1;
- public static final short TYPE_REACTION = 2;
- public static final short TYPE_CONNECTION = 3;
- public static final short TYPE_USER_PARAM = 4;
-
- private static final byte[] ZERO32 = new byte[32];
- private static final String ZERO64 = "0".repeat(64);
-
- // global chain
- private int lastBlockNumber = -1;
- private String lastBlockHashHex = ZERO64;
-
- // header (block#0)
- private byte[] headerHash32 = null;
-
- private final Map hash32ByNumber = new HashMap<>();
-
- // ---------- TECH line state ----------
- private static final class TechLineState {
- int lastGlobalNumber = -1; // последний TECH-блок (HEADER или CREATE_CHANNEL)
- String lastHashHex = "";
- int lastThisLineNumber = 0; // 0 у HEADER (логически), дальше 1,2,3...
-
- void reset() {
- lastGlobalNumber = -1;
- lastHashHex = "";
- lastThisLineNumber = 0;
- }
- }
-
- private final TechLineState techLine = new TechLineState();
-
- // ---------- CONNECTION/USER_PARAM line state ----------
- private static final class SimpleLineState {
- int lastGlobalNumber = -1;
- String lastHashHex = "";
- int lastThisLineNumber = 0;
-
- void reset() {
- lastGlobalNumber = -1;
- lastHashHex = "";
- lastThisLineNumber = 0;
- }
- }
-
- private final SimpleLineState connectionLine = new SimpleLineState();
- private final SimpleLineState userParamLine = new SimpleLineState();
-
- // ---------- TEXT channels ----------
- public static final class ChannelLineState {
- final int lineCode; // для каналов: = rootBlockNumber; для канала 0: 0
- final int rootBlockNumber; // 0 для канала 0, иначе blockNumber CREATE_CHANNEL
- final String rootHashHex;
-
- int lastGlobalNumber;
- String lastHashHex;
- int lastThisLineNumber; // перед первым постом = -1, чтобы первый был 0
-
- ChannelLineState(int lineCode, int rootBlockNumber, String rootHashHex) {
- this.lineCode = lineCode;
- this.rootBlockNumber = rootBlockNumber;
- this.rootHashHex = rootHashHex;
- this.lastGlobalNumber = rootBlockNumber;
- this.lastHashHex = rootHashHex;
- this.lastThisLineNumber = -1;
- }
- }
-
- // lineCode -> state (для канала 0 lineCode=0)
- private final Map textChannels = new HashMap<>();
-
- public ChainState() {
- techLine.reset();
- connectionLine.reset();
- userParamLine.reset();
- }
-
- // -------------------- global getters --------------------
-
- public int lastBlockNumber() { return lastBlockNumber; }
- public String lastBlockHashHex() { return lastBlockHashHex; }
-
- public boolean hasHeader() {
- return headerHash32 != null && headerHash32.length == 32 && lastBlockNumber >= 0;
- }
-
- public int nextBlockNumber() {
- return lastBlockNumber + 1;
- }
-
- public byte[] prevHash32ForNext() {
- if (lastBlockNumber < 0) return ZERO32;
- return hexToBytes32(lastBlockHashHex);
- }
-
- public byte[] headerHash32() {
- return headerHash32 == null ? null : headerHash32.clone();
- }
-
- public byte[] getHash32(int blockNumber) {
- byte[] h = hash32ByNumber.get(blockNumber);
- return h == null ? null : h.clone();
- }
-
- // -------------------- line helpers --------------------
-
- public static final class NextLine {
- public final int lineCode;
- public final int prevLineNumber; // GLOBAL blockNumber
- public final byte[] prevLineHash32; // 32 bytes
- public final int thisLineNumber; // внутр. номер линии
-
- public NextLine(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber) {
- this.lineCode = lineCode;
- this.prevLineNumber = prevLineNumber;
- this.prevLineHash32 = (prevLineHash32 == null ? null : prevLineHash32.clone());
- this.thisLineNumber = thisLineNumber;
- }
- }
-
- /** Следующие line-поля для TECH/CONNECTION/USER_PARAM. lineCode=0. */
- public NextLine nextLineByType(short type) {
- if (!hasHeader()) {
- throw new IllegalStateException("Нельзя формировать line-поля до HEADER (нет headerHash32)");
- }
-
- int t = type & 0xFFFF;
-
- if (t == TYPE_TECH) {
- if (techLine.lastGlobalNumber == -1) {
- throw new IllegalStateException("TECH line is not initialized yet");
- }
- return new NextLine(
- 0,
- techLine.lastGlobalNumber,
- hexToBytes32(techLine.lastHashHex),
- techLine.lastThisLineNumber + 1
- );
- }
-
- if (t == TYPE_CONNECTION) {
- return nextSimpleLine(connectionLine);
- }
- if (t == TYPE_USER_PARAM) {
- return nextSimpleLine(userParamLine);
- }
-
- throw new IllegalArgumentException("Type " + t + " не поддерживает nextLineByType()");
- }
-
- private NextLine nextSimpleLine(SimpleLineState ls) {
- if (ls.lastGlobalNumber == -1) {
- // первый блок линии ссылается на HEADER (block#0)
- return new NextLine(0, 0, headerHash32.clone(), 1);
- }
- if (ls.lastHashHex == null || ls.lastHashHex.isBlank()) {
- throw new IllegalStateException("LineState.lastHashHex пуст, но lastGlobalNumber!=-1");
- }
- return new NextLine(0, ls.lastGlobalNumber, hexToBytes32(ls.lastHashHex), ls.lastThisLineNumber + 1);
- }
-
- /**
- * Следующие line-поля для TEXT-канала по lineCode.
- * Для канала 0: lineCode=0.
- * Для других каналов: lineCode = rootBlockNumber (CREATE_CHANNEL blockNumber).
- */
- public NextLine nextTextLineByCode(int lineCode) {
- if (!hasHeader()) throw new IllegalStateException("No HEADER");
- ChannelLineState cs = textChannels.get(lineCode);
- if (cs == null) throw new IllegalStateException("Unknown TEXT channel lineCode=" + lineCode);
-
- return new NextLine(
- lineCode,
- cs.lastGlobalNumber,
- hexToBytes32(cs.lastHashHex),
- cs.lastThisLineNumber + 1
- );
- }
-
- /** Старое имя — оставил для удобства: rootBlockNumber == lineCode для каналов. */
- public NextLine nextTextLineByRoot(int rootBlockNumber) {
- return nextTextLineByCode(rootBlockNumber);
- }
-
- /**
- * Зарегистрировать новый канал TEXT:
- * - lineCode = rootBlockNumber (blockNumber CREATE_CHANNEL)
- * ИДЕМПОТЕНТНО: если уже зарегистрирован — ничего не делаем.
- */
- public void registerTextChannelRoot(int rootBlockNumber, byte[] rootHash32) {
- if (rootBlockNumber < 0) throw new IllegalArgumentException("rootBlockNumber must be >= 0");
- if (rootHash32 == null || rootHash32.length != 32) throw new IllegalArgumentException("rootHash32 invalid");
-
- if (textChannels.containsKey(rootBlockNumber)) {
- return; // уже есть — не трогаем, чтобы не сбросить lastThisLineNumber и т.д.
- }
-
- int lineCode = rootBlockNumber;
- textChannels.put(lineCode, new ChannelLineState(lineCode, rootBlockNumber, bytesToHex64(rootHash32)));
- }
-
- /** root/lineCode канала "0" (по умолчанию) — это HEADER block#0, lineCode=0. */
- public int rootChannel0() {
- return 0;
- }
-
- // -------------------- apply --------------------
-
- public void applyAppendedBlock(int blockNumber, byte[] hash32, boolean isHeader, short type, BodyRecord body) {
- if (hash32 == null || hash32.length != 32) {
- throw new IllegalArgumentException("hash32 must be 32 bytes");
- }
- if (blockNumber != lastBlockNumber + 1) {
- throw new IllegalStateException("blockNumber sequence broken: expected=" + (lastBlockNumber + 1) + " got=" + blockNumber);
- }
-
- if (isHeader) {
- if (blockNumber != 0) throw new IllegalStateException("HEADER must be blockNumber=0");
- headerHash32 = hash32.clone();
- } else {
- if (blockNumber == 0) throw new IllegalStateException("Non-header block can't be blockNumber=0");
- if (headerHash32 == null) throw new IllegalStateException("Header must be sent before non-header blocks");
- }
-
- String hex64 = bytesToHex64(hash32);
-
- lastBlockNumber = blockNumber;
- lastBlockHashHex = hex64;
-
- hash32ByNumber.put(blockNumber, hash32.clone());
-
- // ---- init after HEADER ----
- if (isHeader) {
- // TECH line root = HEADER
- techLine.lastGlobalNumber = 0;
- techLine.lastHashHex = hex64;
- techLine.lastThisLineNumber = 0;
-
- // TEXT channel "0" root = HEADER, lineCode=0
- registerTextChannelRoot(0, hash32);
-
- return;
- }
-
- int t = type & 0xFFFF;
-
- // ---- TECH (CREATE_CHANNEL) ----
- if (t == TYPE_TECH && body instanceof CreateChannelBody ccb) {
- techLine.lastGlobalNumber = blockNumber;
- techLine.lastHashHex = hex64;
- techLine.lastThisLineNumber = ccb.thisLineNumber;
-
- // ВАЖНО: CREATE_CHANNEL — это root нового текстового канала:
- // lineCode для этого канала = blockNumber CREATE_CHANNEL
- registerTextChannelRoot(blockNumber, hash32);
-
- return;
- }
-
- // ---- CONNECTION / USER_PARAM ----
- if (t == TYPE_CONNECTION && body instanceof BodyHasLine hlc) {
- connectionLine.lastGlobalNumber = blockNumber;
- connectionLine.lastHashHex = hex64;
- connectionLine.lastThisLineNumber = hlc.lineSeq();
- return;
- }
- if (t == TYPE_USER_PARAM && body instanceof BodyHasLine hlu) {
- userParamLine.lastGlobalNumber = blockNumber;
- userParamLine.lastHashHex = hex64;
- userParamLine.lastThisLineNumber = hlu.lineSeq();
- return;
- }
-
- // ---- TEXT channels (POST/EDIT_POST) ----
- if (t == TYPE_TEXT && body instanceof TextBody tb) {
- if (tb.isLineMessage()) {
- int lineCode = tb.lineCode;
-
- ChannelLineState channel = textChannels.get(lineCode);
- if (channel == null) {
- throw new IllegalStateException(
- "TEXT line message has unknown lineCode=" + lineCode +
- " (канал не зарегистрирован; ждали CREATE_CHANNEL или HEADER)"
- );
- }
-
- channel.lastGlobalNumber = blockNumber;
- channel.lastHashHex = hex64;
- channel.lastThisLineNumber = tb.thisLineNumber;
- }
- }
- }
-
- // -------------------- utils --------------------
-
- private static byte[] hexToBytes32(String hex) {
- if (hex == null) throw new IllegalArgumentException("hex is null");
- String s = hex.trim();
- if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
- byte[] out = new byte[32];
- for (int i = 0; i < 32; i++) {
- int hi = Character.digit(s.charAt(i * 2), 16);
- int lo = Character.digit(s.charAt(i * 2 + 1), 16);
- if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
- out[i] = (byte) ((hi << 4) | lo);
- }
- return out;
- }
-
- private static String bytesToHex64(byte[] b32) {
- char[] out = new char[64];
- final char[] HEX = "0123456789abcdef".toCharArray();
- for (int i = 0; i < 32; i++) {
- int v = b32[i] & 0xFF;
- out[i * 2] = HEX[v >>> 4];
- out[i * 2 + 1] = HEX[v & 0x0F];
- }
- return new String(out);
- }
-}
-package test.it.blockchain;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-/**
- * JsonMini — маленькие утилиты, чтобы не раздувать зависимости.
- */
-final class JsonMini {
- private static final ObjectMapper M = new ObjectMapper();
- private JsonMini() {}
-
- static String extractPayloadString(String json, String field) {
- try {
- JsonNode root = M.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload != null && payload.has(field)) {
- JsonNode v = payload.get(field);
- return (v == null || v.isNull()) ? null : v.asText();
- }
- } catch (Exception ignore) {}
- return null;
- }
-}
-package test.it.cases;
-
-import test.it.utils.TestConfig;
-import test.it.utils.json.JsonBuilders;
-import test.it.utils.json.JsonParsers;
-import test.it.utils.log.TestResult;
-import test.it.utils.ws.WsSession;
-
-import java.time.Duration;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * IT_01_AddUser
- * Создаёт 3 пользователей: TestUser1/2/3 (200 OK или 409 USER_ALREADY_EXISTS).
- *
- * Обновление:
- * - теперь AddUser может вернуть 409 не только USER_ALREADY_EXISTS,
- * но и BLOCKCHAIN_ALREADY_EXISTS / BLOCKCHAIN_STATE_ALREADY_EXISTS.
- * - дополнительно проверяем GetUser (status=200 всегда).
- * - добавлен SearchUsers: поиск по префиксу (первые 3 символа).
- */
-public class IT_01_AddUser {
-
- public static void main(String[] args) {
- String summary = run();
- System.out.println(summary);
- }
-
- public static String run() {
- TestResult r = new TestResult("IT_01_AddUser");
-
- Duration t = Duration.ofSeconds(5);
-
- try (WsSession ws = WsSession.open()) {
-
- r.ok("AddUser USER1: " + TestConfig.LOGIN());
- String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t);
- checkAddUser200or409(r, resp1);
- checkGetUserMustExist(r, ws, TestConfig.LOGIN(), t);
-
- r.ok("AddUser USER2: " + TestConfig.LOGIN2());
- String resp2 = ws.call("AddUser#USER2", JsonBuilders.addUser(TestConfig.LOGIN2()), t);
- checkAddUser200or409(r, resp2);
- checkGetUserMustExist(r, ws, TestConfig.LOGIN2(), t);
-
- r.ok("AddUser USER3: " + TestConfig.LOGIN3());
- String resp3 = ws.call("AddUser#USER3", JsonBuilders.addUser(TestConfig.LOGIN3()), t);
- checkAddUser200or409(r, resp3);
- checkGetUserMustExist(r, ws, TestConfig.LOGIN3(), t);
-
- // Доп: проверяем case-insensitive поиск в GetUser
- String mixed = mixCase(TestConfig.LOGIN());
- r.ok("GetUser case-insensitive: запрос=" + mixed + " (должен найти " + TestConfig.LOGIN() + ")");
- checkGetUserMustExist(r, ws, mixed, t);
-
- // Доп: проверяем "не существует" (но status=200)
- String missing = "NoSuchUser_987654321";
- r.ok("GetUser missing: " + missing);
- checkGetUserMustNotExist(r, ws, missing, t);
-
- // SearchUsers: один раз ищем по первым трём символам логина USER1
- String prefix3 = first3(TestConfig.LOGIN());
- String prefix3Mixed = mixCase(prefix3);
- r.ok("SearchUsers: prefix(3)='" + prefix3Mixed + "' (должен вернуть список и содержать " + TestConfig.LOGIN() + ")");
- checkSearchUsersMustContain(r, ws, prefix3Mixed, TestConfig.LOGIN(), t);
-
- } catch (Throwable e) {
- r.fail("IT_01_AddUser упал: " + e.getMessage());
- }
-
- return r.summaryLine();
- }
-
- private static void checkAddUser200or409(TestResult r, String resp) {
- int st = JsonParsers.status(resp);
- if (st == 200) {
- r.ok("AddUser: status=200 (создан)");
- return;
- }
- if (st == 409) {
- String code = JsonParsers.errorCode(resp);
-
- // раньше был только USER_ALREADY_EXISTS, теперь добавились ещё варианты
- if ("USER_ALREADY_EXISTS".equals(code)) {
- r.ok("AddUser: status=409 USER_ALREADY_EXISTS (уже был)");
- return;
- }
- if ("BLOCKCHAIN_ALREADY_EXISTS".equals(code)) {
- r.ok("AddUser: status=409 BLOCKCHAIN_ALREADY_EXISTS (blockchainName уже занят)");
- return;
- }
- if ("BLOCKCHAIN_STATE_ALREADY_EXISTS".equals(code)) {
- r.ok("AddUser: status=409 BLOCKCHAIN_STATE_ALREADY_EXISTS (blockchain_state уже есть)");
- return;
- }
-
- r.fail("AddUser: status=409 но code=" + code + ", resp=" + resp);
- fail("AddUser unexpected 409 code=" + code);
- }
- r.fail("AddUser: неожиданный status=" + st + ", resp=" + resp);
- fail("AddUser unexpected status=" + st);
- }
-
- private static void checkGetUserMustExist(TestResult r, WsSession ws, String loginQuery, Duration t) {
- String resp = ws.call("GetUser#" + loginQuery, JsonBuilders.getUser(loginQuery), t);
-
- int st = JsonParsers.status(resp);
- if (st != 200) {
- r.fail("GetUser: ожидали status=200, получили " + st + ", resp=" + resp);
- fail("GetUser unexpected status=" + st);
- }
-
- Boolean exists = JsonParsers.exists(resp);
- if (exists == null || !exists) {
- r.fail("GetUser: ожидали exists=true, resp=" + resp);
- fail("GetUser expected exists=true");
- }
-
- // Проверяем, что сервер возвращает данные
- String login = JsonParsers.userLogin(resp);
- String blockchainName = JsonParsers.userBlockchainName(resp);
- String solanaKey = JsonParsers.userSolanaKey(resp);
- String blockchainKey = JsonParsers.userBlockchainKey(resp);
- String deviceKey = JsonParsers.userDeviceKey(resp);
-
- if (isBlank(login) || isBlank(blockchainName) || isBlank(solanaKey) || isBlank(blockchainKey) || isBlank(deviceKey)) {
- r.fail("GetUser: exists=true, но поля пустые/неполные, resp=" + resp);
- fail("GetUser returned incomplete user data");
- }
-
- // ВАЖНО:
- // Поиск делается без учета регистра, но login/blockchainName должны вернуться как в БД.
- // Для тех логинов, которые мы создаем в тесте, это ровно TestConfig.LOGIN*().
- // Поэтому если запрос был смешанный регистр — сравниваем не с loginQuery, а с "каноничным" логином из конфига.
- String canonical = canonicalLogin(loginQuery);
- if (canonical != null) {
- if (!login.equals(canonical)) {
- r.fail("GetUser: login должен вернуться как в БД. expected=" + canonical + ", got=" + login + ", resp=" + resp);
- fail("GetUser wrong login case");
- }
-
- String expectedBch = TestConfig.getBlockchainName(canonical);
- if (!blockchainName.equals(expectedBch)) {
- r.fail("GetUser: blockchainName должен вернуться как в БД. expected=" + expectedBch + ", got=" + blockchainName + ", resp=" + resp);
- fail("GetUser wrong blockchainName");
- }
-
- // ключи должны совпадать с теми, что AddUser использует при регистрации
- String expSol = TestConfig.solanaPublicKeyB64(canonical);
- String expBchKey = TestConfig.blockchainPublicKeyB64(canonical);
- String expDev = TestConfig.devicePublicKeyB64(canonical);
-
- if (!solanaKey.equals(expSol)) {
- r.fail("GetUser: solanaKey mismatch, resp=" + resp);
- fail("GetUser solanaKey mismatch");
- }
- if (!blockchainKey.equals(expBchKey)) {
- r.fail("GetUser: blockchainKey mismatch, resp=" + resp);
- fail("GetUser blockchainKey mismatch");
- }
- if (!deviceKey.equals(expDev)) {
- r.fail("GetUser: deviceKey mismatch, resp=" + resp);
- fail("GetUser deviceKey mismatch");
- }
- }
-
- r.ok("GetUser: exists=true, login=" + login + ", blockchainName=" + blockchainName);
- }
-
- private static void checkGetUserMustNotExist(TestResult r, WsSession ws, String loginQuery, Duration t) {
- String resp = ws.call("GetUser#" + loginQuery, JsonBuilders.getUser(loginQuery), t);
-
- int st = JsonParsers.status(resp);
- if (st != 200) {
- r.fail("GetUser(not exist): ожидали status=200, получили " + st + ", resp=" + resp);
- fail("GetUser(not exist) unexpected status=" + st);
- }
-
- Boolean exists = JsonParsers.exists(resp);
- if (exists == null) {
- r.fail("GetUser(not exist): payload.exists отсутствует, resp=" + resp);
- fail("GetUser(not exist) missing exists");
- }
- if (exists) {
- r.fail("GetUser(not exist): ожидали exists=false, resp=" + resp);
- fail("GetUser(not exist) expected exists=false");
- }
-
- r.ok("GetUser: exists=false (ok)");
- }
-
- private static void checkSearchUsersMustContain(TestResult r, WsSession ws, String prefix, String expectedLogin, Duration t) {
- String resp = ws.call("SearchUsers#" + prefix, JsonBuilders.searchUsers(prefix), t);
-
- int st = JsonParsers.status(resp);
- if (st != 200) {
- r.fail("SearchUsers: ожидали status=200, получили " + st + ", resp=" + resp);
- fail("SearchUsers unexpected status=" + st);
- }
-
- List logins = JsonParsers.searchLogins(resp);
- if (logins == null || logins.isEmpty()) {
- r.fail("SearchUsers: ожидали непустой список, resp=" + resp);
- fail("SearchUsers expected non-empty list");
- }
-
- // ВАЖНО: ожидаемый логин должен быть в ответе в регистре БД (каноничный expectedLogin)
- boolean found = false;
- for (String s : logins) {
- if (expectedLogin.equals(s)) {
- found = true;
- break;
- }
- }
- if (!found) {
- r.fail("SearchUsers: ожидаемый логин не найден. expected=" + expectedLogin + ", got=" + logins + ", resp=" + resp);
- fail("SearchUsers expected login not found");
- }
-
- r.ok("SearchUsers: ok, prefix=" + prefix + ", results=" + logins.size() + ", contains=" + expectedLogin);
- }
-
- private static String canonicalLogin(String anyCaseLogin) {
- if (anyCaseLogin == null) return null;
- String x = anyCaseLogin.trim();
- if (x.isEmpty()) return null;
-
- // Привязка только к нашим тестовым логинам, чтобы не гадать.
- if (x.equalsIgnoreCase(TestConfig.LOGIN())) return TestConfig.LOGIN();
- if (x.equalsIgnoreCase(TestConfig.LOGIN2())) return TestConfig.LOGIN2();
- if (x.equalsIgnoreCase(TestConfig.LOGIN3())) return TestConfig.LOGIN3();
-
- return null;
- }
-
- private static String mixCase(String s) {
- if (s == null) return null;
- String x = s.trim();
- if (x.length() < 2) return x;
- // простой "микс" без рандома, чтобы тест был детерминированный
- return Character.toUpperCase(x.charAt(0)) + x.substring(1).toLowerCase();
- }
-
- private static String first3(String s) {
- if (s == null) return "";
- String x = s.trim();
- if (x.length() <= 3) return x;
- return x.substring(0, 3);
- }
-
- private static boolean isBlank(String s) {
- return s == null || s.trim().isEmpty();
- }
-}
-package test.it.cases;
-
-import test.it.utils.TestConfig;
-import test.it.utils.json.JsonBuilders;
-import test.it.utils.json.JsonParsers;
-import test.it.utils.log.TestLog;
-import test.it.utils.log.TestResult;
-import test.it.utils.ws.WsSession;
-
-import java.time.Duration;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * IT_02_Sessions (v2)
- *
- * Цель:
- * - проверить создание/листинг/вход-в-сессию(2 шага)/close
- * - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
- *
- * Протокол v2:
- * - создание сессии: AuthChallenge -> CreateAuthSession (deviceKey подпись, + sessionPubKey)
- * - вход в сессию: SessionChallenge(sessionId) -> nonce, затем SessionLogin(sessionId,time,signature(sessionKey))
- * - ListSessions и CloseActiveSession доступны только в AUTH_STATUS_USER (после SessionLogin)
- */
-public class IT_02_Sessions {
-
- private static final String LOGIN = TestConfig.LOGIN();
-
- public static void main(String[] args) {
- TestLog.info("Standalone: этот тест требует заранее созданных пользователей -> сначала запускаю IT_01_AddUser");
- System.out.println(IT_01_AddUser.run());
- String summary = run();
- System.out.println(summary);
- }
-
- public static String run() {
- TestResult r = new TestResult("IT_02_Sessions(v2)");
-
- Duration t = Duration.ofSeconds(5);
-
- Session s1, s2, s3;
-
- try {
- // 1) Создаём 3 сессии (каждая — отдельным соединением)
- s1 = createSession(LOGIN, t, r, "S1");
- s2 = createSession(LOGIN, t, r, "S2");
- s3 = createSession(LOGIN, t, r, "S3");
-
- // 2) Входим в S1 (2 шага) и делаем ListSessions (AUTH_STATUS_USER) — должны быть S1,S2,S3
- try (WsSession ws = WsSession.open()) {
- sessionLogin2Steps(ws, s1, t, "Login(S1)", r);
-
- String listResp = ws.call("ListSessions(AUTH_STATUS_USER)", JsonBuilders.listSessions(0L, ""), t);
- assertEquals(200, JsonParsers.status(listResp), "ListSessions(AUTH_STATUS_USER) must be 200");
-
- List ids = JsonParsers.sessionIds(listResp);
- r.ok("ListSessions(AUTH_STATUS_USER): " + ids);
-
- assertTrue(ids.contains(s1.sessionId), "Must contain S1");
- assertTrue(ids.contains(s2.sessionId), "Must contain S2");
- assertTrue(ids.contains(s3.sessionId), "Must contain S3");
- r.ok("Проверка OK: список содержит S1,S2,S3");
- }
-
- // 3) Проверяем CloseActiveSession так, чтобы итогом всё равно осталось 3 сессии:
- // создаём TEMP, логинимся в S1, закрываем TEMP, убеждаемся что S1,S2,S3 остались.
- Session temp = createSession(LOGIN, t, r, "TEMP");
-
- try (WsSession ws = WsSession.open()) {
- sessionLogin2Steps(ws, s1, t, "Login(S1) for close", r);
-
- String closeResp = ws.call("CloseActiveSession(TEMP)", JsonBuilders.closeActiveSession(temp.sessionId, 0L, ""), t);
- assertEquals(200, JsonParsers.status(closeResp), "CloseActiveSession(TEMP) must be 200");
- r.ok("CloseActiveSession(TEMP): OK");
- }
-
- // 4) Финальная проверка: снова логинимся в S1 и ListSessions => S1,S2,S3 должны остаться, TEMP нет
- try (WsSession ws = WsSession.open()) {
- sessionLogin2Steps(ws, s1, t, "Final Login(S1)", r);
-
- String listResp = ws.call("ListSessions(final)", JsonBuilders.listSessions(0L, ""), t);
- assertEquals(200, JsonParsers.status(listResp));
-
- List ids = JsonParsers.sessionIds(listResp);
- r.ok("Final ListSessions: " + ids);
-
- assertTrue(ids.contains(s1.sessionId));
- assertTrue(ids.contains(s2.sessionId));
- assertTrue(ids.contains(s3.sessionId));
- assertFalse(ids.contains(temp.sessionId));
- r.ok("ИТОГ OK: после теста в БД остались 3 активные сессии (S1,S2,S3)");
- }
-
- } catch (Throwable e) {
- r.fail("IT_02_Sessions(v2) упал: " + e.getMessage());
- }
-
- return r.summaryLine();
- }
-
- private static Session createSession(String login, Duration t, TestResult r, String label) {
- try (WsSession ws = WsSession.open()) {
-
- // шаг 1: AuthChallenge
- String nonceResp = ws.call("AuthChallenge(" + label + ")", JsonBuilders.authChallenge(login), t);
- assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(" + label + ") must be 200");
- String authNonce = JsonParsers.authNonce(nonceResp);
- assertNotNull(authNonce, "authNonce must not be null for " + label);
-
- // для тестов: sessionKey = deviceKey (в реале будет отдельный keypair)
- String sessionPubKeyB64 = TestConfig.devicePublicKeyB64(login);
-
- // storagePwd на клиенте (сохраняем, чтобы потом проверить, что сервер вернул именно его)
- String storagePwd = TestConfig.fakeStoragePwd();
-
- // шаг 2: CreateAuthSession (device подпись + sessionPubKey)
- String createResp = ws.call(
- "CreateAuthSession(" + label + ")",
- JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionPubKeyB64),
- t
- );
- assertEquals(200, JsonParsers.status(createResp), "CreateAuthSession(" + label + ") must be 200");
-
- String sid = JsonParsers.sessionId(createResp);
- assertNotNull(sid, "sessionId must not be null");
-
- r.ok("Создана сессия " + label + ": sessionId=" + sid);
-
- // для тестов используем devicePriv как sessionPriv
- byte[] sessionPrivKey = TestConfig.getDevicePrivatKey(login);
-
- return new Session(sid, sessionPrivKey, storagePwd);
- }
- }
-
- private static void sessionLogin2Steps(WsSession ws, Session s, Duration t, String label, TestResult r) {
- // шаг 1: SessionChallenge(sessionId)
- String chResp = ws.call("SessionChallenge " + label, JsonBuilders.sessionChallenge(s.sessionId), t);
- assertEquals(200, JsonParsers.status(chResp), "SessionChallenge must be 200");
- String nonce = JsonParsers.sessionNonce(chResp);
- assertNotNull(nonce, "SessionChallenge nonce must not be null");
-
- // шаг 2: SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
- String loginResp = ws.call("SessionLogin " + label, JsonBuilders.sessionLogin(s.sessionId, nonce, s.sessionPrivKey), t);
- assertEquals(200, JsonParsers.status(loginResp), "SessionLogin must be 200");
-
- String storagePwd = JsonParsers.storagePwd(loginResp);
- assertNotNull(storagePwd, "storagePwd must not be null after SessionLogin");
- assertEquals(s.storagePwd, storagePwd, "storagePwd must match what client provided on CreateAuthSession");
-
- r.ok(label + ": SessionLogin OK, storagePwd verified");
- }
-
- private record Session(String sessionId, byte[] sessionPrivKey, String storagePwd) {}
-}
-package test.it.cases;
-
-import blockchain.MsgSubType;
-import blockchain.body.ConnectionBody;
-import blockchain.body.CreateChannelBody;
-import blockchain.body.HeaderBody;
-import blockchain.body.TextBody;
-import test.it.blockchain.AddBlockSender;
-import test.it.blockchain.ChainState;
-import test.it.utils.TestConfig;
-import test.it.utils.log.TestLog;
-import test.it.utils.log.TestResult;
-import test.it.utils.ws.WsSession;
-
-import java.time.Duration;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * IT_03_AddBlock_NoAuth — сценарий блоков (новый формат + каналы + связи).
- *
- * CONNECTION (type=3):
- * - всегда имеет hasLine (lineCode+prevLineNumber+prevLineHash32+thisLineNumber)
- * - всегда имеет target:
- * toBlockchainName + toBlockGlobalNumber + toBlockHash32
- *
- * Правило target для связей/подписок:
- * - FRIEND/CONTACT -> target = HEADER цели (blockNumber=0)
- * - FOLLOW пользователя -> target = HEADER цели (blockNumber=0)
- * - FOLLOW канала -> target = ROOT канала:
- * канал "0" -> HEADER (0)
- * канал "X" -> CREATE_CHANNEL (blockNumber create_channel)
- */
-public class IT_03_AddBlock_NoAuth {
-
- public static void main(String[] args) {
- TestLog.info("Standalone: этот тест требует заранее созданных пользователей -> запускаю IT_01_AddUser");
- System.out.println(IT_01_AddUser.run());
- String summary = run();
- System.out.println(summary);
- }
-
- public static String run() {
- TestResult r = new TestResult("IT_03_AddBlock_NoAuth");
-
- String u1 = TestConfig.LOGIN();
- String u2 = TestConfig.LOGIN2();
- String u3 = TestConfig.LOGIN3();
-
- String bch1 = TestConfig.getBlockchainName(u1);
- String bch2 = TestConfig.getBlockchainName(u2);
- String bch3 = TestConfig.getBlockchainName(u3);
-
- Duration t = Duration.ofSeconds(1);
-
- try (WsSession ws = WsSession.open()) {
-
- if (TestConfig.DEBUG()) {
- TestLog.titleBlock(
- "IT_03:\n" +
- " USER1=" + u1 + " bch=" + bch1 + "\n" +
- " USER2=" + u2 + " bch=" + bch2 + "\n" +
- " USER3=" + u3 + " bch=" + bch3 + "\n" +
- "\nСценарий: каналы + кросс-чейн reply + connections (follow/friend/contact/uncontact)."
- );
- }
-
- // =========================
- // USER1
- // =========================
- ChainState st1 = new ChainState();
- AddBlockSender sender1 = new AddBlockSender(ws, st1, u1, bch1, TestConfig.getBlockchainPrivatKey(u1));
-
- sender1.send(new HeaderBody(u1), t);
- assertTrue(st1.hasHeader());
-
- int u1HeaderBlock = 0;
- byte[] u1HeaderHash = st1.getHash32(u1HeaderBlock);
- assertNotNull(u1HeaderHash);
-
- // канал "0" root = HEADER (0)
- int root0 = st1.rootChannel0();
-
- // POST в канал "0"
- {
- var ln = st1.nextTextLineByRoot(root0);
- sender1.send(new TextBody(
- MsgSubType.TEXT_POST,
- root0,
- ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
- "U1: story/post in channel 0",
- null, null, null
- ), t);
- }
-
- int post0Block = st1.lastBlockNumber();
- byte[] post0Hash = st1.getHash32(post0Block);
- assertNotNull(post0Hash);
-
- // CREATE_CHANNEL "News" (TECH line)
- int newsRootBlock;
- byte[] newsRootHash;
- {
- var ln = st1.nextLineByType(ChainState.TYPE_TECH);
- sender1.send(new CreateChannelBody(
- 0, // lineCode TECH
- ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
- "News"
- ), t);
-
- newsRootBlock = st1.lastBlockNumber(); // root канала = blockNumber этого CREATE_CHANNEL
- newsRootHash = st1.getHash32(newsRootBlock);
- assertNotNull(newsRootHash);
-
- st1.registerTextChannelRoot(newsRootBlock, newsRootHash);
- }
-
- // POST #0 в канал "News"
- int newsPost0Block;
- byte[] newsPost0Hash;
- {
- var ln = st1.nextTextLineByRoot(newsRootBlock);
- sender1.send(new TextBody(
- MsgSubType.TEXT_POST,
- newsRootBlock,
- ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
- "U1: News post #0",
- null, null, null
- ), t);
-
- newsPost0Block = st1.lastBlockNumber();
- newsPost0Hash = st1.getHash32(newsPost0Block);
- assertNotNull(newsPost0Hash);
- }
-
- // POST #1 в канал "News"
- {
- var ln = st1.nextTextLineByRoot(newsRootBlock);
- sender1.send(new TextBody(
- MsgSubType.TEXT_POST,
- newsRootBlock,
- ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
- "U1: News post #1",
- null, null, null
- ), t);
- }
-
- // EDIT_POST (в линии канала) -> target на ОРИГИНАЛЬНЫЙ POST (без toBlockchainName)
- {
- var ln = st1.nextTextLineByRoot(newsRootBlock);
- sender1.send(new TextBody(
- MsgSubType.TEXT_EDIT_POST,
- newsRootBlock,
- ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
- "U1: News post #0 (EDIT)",
- null,
- newsPost0Block,
- newsPost0Hash
- ), t);
- }
-
- // =========================
- // USER2
- // =========================
- ChainState st2 = new ChainState();
- AddBlockSender sender2 = new AddBlockSender(ws, st2, u2, bch2, TestConfig.getBlockchainPrivatKey(u2));
-
- sender2.send(new HeaderBody(u2), t);
- assertTrue(st2.hasHeader());
-
- int u2HeaderBlock = 0;
- byte[] u2HeaderHash = st2.getHash32(u2HeaderBlock);
- assertNotNull(u2HeaderHash);
-
- // =========================
- // СВЯЗИ (CONNECTION)
- // =========================
-
- // 1) U1 подписался на U2 (FOLLOW на пользователя -> target=HEADER U2)
- sendConnection(sender1, st1, MsgSubType.CONNECTION_FOLLOW,
- bch2, u2HeaderBlock, u2HeaderHash,
- "U1 follows U2 (target=U2 HEADER)", t);
-
- // 2) U2 подписался на канал U1 "News" (FOLLOW на канал -> target=root CREATE_CHANNEL U1)
- sendConnection(sender2, st2, MsgSubType.CONNECTION_FOLLOW,
- bch1, newsRootBlock, newsRootHash,
- "U2 follows U1 channel 'News' (target=U1 CREATE_CHANNEL root)", t);
-
- // 3) FRIEND взаимно (на HEADER)
- sendConnection(sender1, st1, MsgSubType.CONNECTION_FRIEND,
- bch2, u2HeaderBlock, u2HeaderHash,
- "U1 -> U2: FRIEND", t);
-
- sendConnection(sender2, st2, MsgSubType.CONNECTION_FRIEND,
- bch1, u1HeaderBlock, u1HeaderHash,
- "U2 -> U1: FRIEND", t);
-
- // 4) CONTACT несколько
- sendConnection(sender1, st1, MsgSubType.CONNECTION_CONTACT,
- bch2, u2HeaderBlock, u2HeaderHash,
- "U1 -> U2: CONTACT", t);
-
- sendConnection(sender2, st2, MsgSubType.CONNECTION_CONTACT,
- bch1, u1HeaderBlock, u1HeaderHash,
- "U2 -> U1: CONTACT", t);
-
- // =========================
- // USER2 REPLY (ответ в чужой канал)
- // =========================
- {
- sender2.send(TextBody.newReply(
- bch1,
- newsPost0Block,
- newsPost0Hash,
- "U2: reply to U1 News post #0 (cross-chain)"
- ), t);
- }
-
- // =========================
- // USER3 + доп. контакт
- // =========================
- ChainState st3 = new ChainState();
- AddBlockSender sender3 = new AddBlockSender(ws, st3, u3, bch3, TestConfig.getBlockchainPrivatKey(u3));
-
- sender3.send(new HeaderBody(u3), t);
- assertTrue(st3.hasHeader());
-
- int u3HeaderBlock = 0;
- byte[] u3HeaderHash = st3.getHash32(u3HeaderBlock);
- assertNotNull(u3HeaderHash);
-
- // U1 -> U3: CONTACT
- sendConnection(sender1, st1, MsgSubType.CONNECTION_CONTACT,
- bch3, u3HeaderBlock, u3HeaderHash,
- "U1 -> U3: CONTACT", t);
-
- // 5) U1 убирает U2 из контактов (UNCONTACT)
- sendConnection(sender1, st1, MsgSubType.CONNECTION_UNCONTACT,
- bch2, u2HeaderBlock, u2HeaderHash,
- "U1 -> U2: UNCONTACT", t);
-
- r.ok("IT_03 сценарий блоков + connections выполнен");
-
- } catch (Throwable e) {
- r.fail("IT_03 упал: " + e.getMessage());
- }
-
- return r.summaryLine();
- }
-
- /**
- * Отправка 1 блока CONNECTION.
- *
- * ВАЖНО: ConnectionBody НЕ содержит note в байтах.
- * Если нужно “описание” — логируем отдельно.
- */
- private static void sendConnection(AddBlockSender sender,
- ChainState st,
- short subType,
- String toBlockchainName,
- int toBlockNumber,
- byte[] toBlockHash32,
- String logNote,
- Duration timeout) {
-
- if (TestConfig.DEBUG()) {
- TestLog.info("CONNECTION: subType=" + (subType & 0xFFFF)
- + " to=" + toBlockchainName
- + " targetBlock=" + toBlockNumber
- + " note=" + logNote);
- }
-
- var ln = st.nextLineByType(ChainState.TYPE_CONNECTION);
-
- // КОНСТРУКТОР ИЗ ТВОЕГО КОДА:
- // ConnectionBody(int lineCode, int prevLineNumber, byte[] prevLineHash32, int thisLineNumber,
- // short subType, String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32)
- sender.send(new ConnectionBody(
- 0, // lineCode для connection линии
- ln.prevLineNumber,
- ln.prevLineHash32,
- ln.thisLineNumber,
- subType,
- toBlockchainName,
- toBlockNumber,
- toBlockHash32
- ), timeout);
- }
-}
-package test.it.cases;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import test.it.utils.*;
-import test.it.utils.TestConfig;
-import test.it.utils.json.JsonParsers;
-import test.it.utils.log.TestLog;
-import test.it.utils.log.TestResult;
-import test.it.utils.ws.WsSession;
-import utils.config.ShineSignatureConstants;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.util.Base64;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * IT_04_UserParams_NoAuth
- *
- * ВАЖНО:
- * - пользователей НЕ создаём (их создаёт IT_01)
- */
-public class IT_04_UserParams_NoAuth {
-
- private static final ObjectMapper M = new ObjectMapper();
-
- public static void main(String[] args) {
- TestLog.info("Standalone: этот тест требует заранее созданных пользователей -> сначала запускаю IT_01_AddUser");
- System.out.println(IT_01_AddUser.run());
- String summary = run();
- System.out.println(summary);
- }
-
- public static String run() {
- TestResult r = new TestResult("IT_04_UserParams_NoAuth");
-
- Duration timeout = Duration.ofSeconds(5);
-
- final String login = TestConfig.LOGIN();
- final String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
- final byte[] devicePrivKey = TestConfig.getDevicePrivatKey(login);
-
- try {
- // 1) сохранить param1
- final String p1 = "profile:name";
- final String v1 = "Anna";
- final long t1 = System.currentTimeMillis();
- upsertUserParam_OK(r, login, p1, t1, v1, deviceKeyB64, devicePrivKey, timeout);
-
- // 2) получить param1 и проверить
- NetParam got1 = getUserParam_200(r, login, p1, timeout);
- assertEquals(login, got1.login);
- assertEquals(p1, got1.param);
- assertEquals(t1, got1.timeMs);
- assertEquals(v1, got1.value);
- assertEquals(deviceKeyB64, got1.deviceKeyB64);
- assertNotNull(got1.signatureB64);
- assertFalse(got1.signatureB64.isBlank());
- r.ok("GetUserParam(param1) OK");
-
- // 3) сохранить param2
- final String p2 = "profile:city";
- final String v2 = "Amsterdam";
- final long t2 = t1 + 10;
- upsertUserParam_OK(r, login, p2, t2, v2, deviceKeyB64, devicePrivKey, timeout);
-
- // 4) обновить param1
- final String v1b = "Anna Updated";
- final long t1b = t2 + 10;
- upsertUserParam_OK(r, login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout);
-
- NetParam got1b = getUserParam_200(r, login, p1, timeout);
- assertEquals(t1b, got1b.timeMs);
- assertEquals(v1b, got1b.value);
- r.ok("GetUserParam(updated param1) OK");
-
- // 5) list всех параметров
- NetParamList list = listUserParams_200(r, login, timeout);
-
- NetParam lp1 = list.find(p1);
- NetParam lp2 = list.find(p2);
-
- assertNotNull(lp1, "ListUserParams должен содержать param1=" + p1);
- assertNotNull(lp2, "ListUserParams должен содержать param2=" + p2);
-
- assertEquals(t1b, lp1.timeMs);
- assertEquals(v1b, lp1.value);
-
- assertEquals(t2, lp2.timeMs);
- assertEquals(v2, lp2.value);
-
- assertEquals(deviceKeyB64, lp1.deviceKeyB64);
- assertEquals(deviceKeyB64, lp2.deviceKeyB64);
- assertNotNull(lp1.signatureB64);
- assertNotNull(lp2.signatureB64);
-
- r.ok("ListUserParams OK");
-
- } catch (Throwable e) {
- r.fail("IT_04 упал: " + e.getMessage());
- }
-
- return r.summaryLine();
- }
-
- // =================================================================================
- // WS helpers: Upsert/Get/List
- // =================================================================================
-
- private static void upsertUserParam_OK(TestResult r, String login, String param, long timeMs, String value, String deviceKeyB64, byte[] devicePrivKey, Duration timeout) {
- String signatureB64 = signUserParam(devicePrivKey, login, param, timeMs, value);
-
- String reqJson = """
- {
- "op": "UpsertUserParam",
- "requestId": "%s",
- "payload": {
- "login": "%s",
- "param": "%s",
- "time_ms": %d,
- "value": "%s",
- "device_key": "%s",
- "signature": "%s"
- }
- }
- """.formatted(TestIds.next("upsert"), login, param, timeMs, jsonEscape(value), deviceKeyB64, signatureB64);
-
- try (WsSession ws = WsSession.open()) {
- String resp = ws.call("UpsertUserParam(" + param + ")", reqJson, timeout);
- assertEquals(200, JsonParsers.status(resp), "UpsertUserParam expected 200, resp=" + resp);
- r.ok("UpsertUserParam(" + param + "): OK");
- }
- }
-
- private static NetParam getUserParam_200(TestResult r, String login, String param, Duration timeout) {
- String reqJson = """
- {
- "op": "GetUserParam",
- "requestId": "%s",
- "payload": {
- "login": "%s",
- "param": "%s"
- }
- }
- """.formatted(TestIds.next("getparam"), login, param);
-
- try (WsSession ws = WsSession.open()) {
- String resp = ws.call("GetUserParam(" + param + ")", reqJson, timeout);
- assertEquals(200, JsonParsers.status(resp), "GetUserParam expected 200, resp=" + resp);
- r.ok("GetUserParam(" + param + "): OK");
- return parseParamFromResponsePayload(resp);
- }
- }
-
- private static NetParamList listUserParams_200(TestResult r, String login, Duration timeout) {
- String reqJson = """
- {
- "op": "ListUserParams",
- "requestId": "%s",
- "payload": { "login": "%s" }
- }
- """.formatted(TestIds.next("listparams"), login);
-
- try (WsSession ws = WsSession.open()) {
- String resp = ws.call("ListUserParams", reqJson, timeout);
- assertEquals(200, JsonParsers.status(resp), "ListUserParams expected 200, resp=" + resp);
- r.ok("ListUserParams: OK");
- return parseParamListFromResponsePayload(resp);
- }
- }
-
- // =================================================================================
- // Parsing helpers
- // =================================================================================
-
- private static NetParam parseParamFromResponsePayload(String respJson) {
- try {
- JsonNode root = M.readTree(respJson);
- JsonNode payload = root.get("payload");
- assertNotNull(payload, "payload is null: " + respJson);
-
- NetParam p = new NetParam();
- p.login = text(payload, "login");
- p.param = text(payload, "param");
- p.timeMs = longVal(payload, "time_ms");
- p.value = text(payload, "value");
- p.deviceKeyB64 = text(payload, "device_key");
- p.signatureB64 = text(payload, "signature");
- return p;
- } catch (Exception e) {
- throw new RuntimeException("Failed to parse GetUserParam response: " + respJson, e);
- }
- }
-
- private static NetParamList parseParamListFromResponsePayload(String respJson) {
- try {
- JsonNode root = M.readTree(respJson);
- JsonNode payload = root.get("payload");
- assertNotNull(payload, "payload is null: " + respJson);
-
- NetParamList out = new NetParamList();
- out.login = text(payload, "login");
-
- JsonNode arr = payload.get("params");
- assertNotNull(arr, "payload.params is null: " + respJson);
- assertTrue(arr.isArray(), "payload.params must be array: " + respJson);
-
- for (JsonNode it : arr) {
- NetParam p = new NetParam();
- p.login = text(it, "login");
- p.param = text(it, "param");
- p.timeMs = longVal(it, "time_ms");
- p.value = text(it, "value");
- p.deviceKeyB64 = text(it, "device_key");
- p.signatureB64 = text(it, "signature");
- out.items = out.itemsAppend(p);
- }
- return out;
- } catch (Exception e) {
- throw new RuntimeException("Failed to parse ListUserParams response: " + respJson, e);
- }
- }
-
- private static String text(JsonNode obj, String field) {
- JsonNode v = obj.get(field);
- return (v == null || v.isNull()) ? null : v.asText();
- }
-
- private static long longVal(JsonNode obj, String field) {
- JsonNode v = obj.get(field);
- if (v == null || v.isNull()) return 0;
- return v.asLong();
- }
-
- // =================================================================================
- // Signature + JSON helpers
- // =================================================================================
-
- private static String signUserParam(byte[] devicePrivKey, String login, String param, long timeMs, String value) {
- String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX + login + param + timeMs + value;
- byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
- byte[] sig64 = Ed25519Util.sign(signBytes, devicePrivKey);
- return Base64.getEncoder().encodeToString(sig64);
- }
-
- private static String jsonEscape(String s) {
- if (s == null) return "";
- return s.replace("\\", "\\\\").replace("\"", "\\\"");
- }
-
- // =================================================================================
- // DTOs
- // =================================================================================
-
- private static final class NetParam {
- String login;
- String param;
- long timeMs;
- String value;
- String deviceKeyB64;
- String signatureB64;
- }
-
- private static final class NetParamList {
- String login;
- NetParam[] items = new NetParam[0];
-
- NetParam[] itemsAppend(NetParam p) {
- NetParam[] n = new NetParam[items.length + 1];
- System.arraycopy(items, 0, n, 0, items.length);
- n[items.length] = p;
- items = n;
- return items;
- }
-
- NetParam find(String param) {
- for (NetParam p : items) {
- if (p != null && param.equals(p.param)) return p;
- }
- return null;
- }
- }
-}
-package test.it.cases;
-
-import test.it.utils.TestConfig;
-import test.it.utils.json.JsonBuilders;
-import test.it.utils.json.JsonParsers;
-import test.it.utils.log.TestResult;
-import test.it.utils.ws.WsSession;
-
-import java.time.Duration;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * IT_05_UserConnections
- *
- * Делает пару запросов GetFriendsLists (без проверок существования юзеров — это уже в IT_01).
- *
- * Ожидаемый формат ответа:
- * {
- * "op":"GetFriendsLists",
- * "requestId":"...",
- * "status":200,
- * "payload":{
- * "login":"TestUser1", // канонический регистр из БД
- * "out_friends":[...], // кому login поставил FRIEND
- * "in_friends":[...] // кто поставил FRIEND login
- * }
- * }
- *
- * ВАЖНО:
- * - login в запросе может быть в любом регистре,
- * - но в ответе payload.login должен быть канонический (как в БД).
- */
-public class IT_05_UserConnections {
-
- public static void main(String[] args) {
- String summary = run();
- System.out.println(summary);
- }
-
- public static String run() {
- TestResult r = new TestResult("IT_05_UserConnections");
- Duration t = Duration.ofSeconds(5);
-
- final String u1 = TestConfig.LOGIN();
- final String u2 = TestConfig.LOGIN2();
-
- try (WsSession ws = WsSession.open()) {
-
- // 1) Запрос списков связей для u1 (канонический регистр)
- r.ok("GetFriendsLists USER1: " + u1);
- String resp1 = ws.call("GetFriendsLists#U1", JsonBuilders.getFriendsLists(u1), t);
- check200(r, resp1);
- checkCanonicalLogin(r, resp1, u1);
- checkTwoListsPresent(r, resp1);
-
- // 2) Запрос списков связей для u1 (смешанный регистр)
- String u1mixed = mixCase(u1);
- r.ok("GetFriendsLists USER1 mixed-case request: " + u1mixed + " (expect login=" + u1 + ")");
- String resp2 = ws.call("GetFriendsLists#U1_MIX", JsonBuilders.getFriendsLists(u1mixed), t);
- check200(r, resp2);
- checkCanonicalLogin(r, resp2, u1);
- checkTwoListsPresent(r, resp2);
-
- // 3) Ещё один запрос — для u2 (просто чтобы "пару запросов")
- r.ok("GetFriendsLists USER2: " + u2);
- String resp3 = ws.call("GetFriendsLists#U2", JsonBuilders.getFriendsLists(u2), t);
- check200(r, resp3);
- checkCanonicalLogin(r, resp3, u2);
- checkTwoListsPresent(r, resp3);
-
- // лог для наглядности (могут быть пустые, это ок)
- List out1 = JsonParsers.friendsOut(resp1);
- List in1 = JsonParsers.friendsIn(resp1);
-
- r.ok("Friends lists USER1: out=" + out1.size() + ", in=" + in1.size());
-
- } catch (Throwable e) {
- r.fail("IT_05_UserConnections упал: " + e.getMessage());
- }
-
- return r.summaryLine();
- }
-
- // ================= checks =================
-
- private static void check200(TestResult r, String resp) {
- int st = JsonParsers.status(resp);
- if (st != 200) {
- r.fail("ожидали status=200, получили " + st + ", resp=" + resp);
- fail("unexpected status=" + st);
- }
- }
-
- private static void checkCanonicalLogin(TestResult r, String resp, String expectedCanonicalLogin) {
- String got = JsonParsers.friendsLogin(resp);
- if (got == null) {
- r.fail("GetFriendsLists: payload.login отсутствует, resp=" + resp);
- fail("GetFriendsLists missing payload.login");
- }
- if (!expectedCanonicalLogin.equals(got)) {
- r.fail("GetFriendsLists: login должен вернуться канонический. expected=" + expectedCanonicalLogin + ", got=" + got + ", resp=" + resp);
- fail("GetFriendsLists wrong login case");
- }
- }
-
- private static void checkTwoListsPresent(TestResult r, String resp) {
- // В JsonParsers.getPayloadStringArray сейчас возвращает пустой список, даже если поле отсутствует/не массив.
- // Поэтому дополнительно проверяем, что парсер вернул НЕ null (он и не должен возвращать null).
- List out = JsonParsers.friendsOut(resp);
- List in = JsonParsers.friendsIn(resp);
-
- if (out == null || in == null) {
- r.fail("GetFriendsLists: out_friends/in_friends не должны быть null, resp=" + resp);
- fail("GetFriendsLists lists are null");
- }
-
- // Просто отмечаем, что поля читаются, даже если пустые.
- r.ok("GetFriendsLists lists present: out=" + out.size() + ", in=" + in.size());
- }
-
- private static String mixCase(String s) {
- if (s == null) return null;
- String x = s.trim();
- if (x.length() < 2) return x;
- return Character.toUpperCase(x.charAt(0)) + x.substring(1).toLowerCase();
- }
-}
-package test.it;
-
-import test.it.runner.IT_RunAllMain;
-
-import java.util.Objects;
-
-public class IT_DeployRestartAndRunRemoteMain {
-
- // ====== НАСТРОЙКИ (можно переопределять systemProperty) ======
- private static final String REMOTE_HOST = System.getProperty("it.remoteHost", "194.87.0.247");
- private static final String REMOTE_USER = System.getProperty("it.remoteUser", "user");
-
- private static final String REMOTE_DIR = System.getProperty("it.remoteDir", "/home/user/docker/shine-server");
- private static final String REMOTE_JAR = REMOTE_DIR + "/shine-server.jar";
- private static final String REMOTE_DATA = System.getProperty("it.remoteDataDir", REMOTE_DIR + "/data");
-
- private static final String SERVICE_NAME = System.getProperty("it.service", "shine-server");
-
- private static final String LOCAL_JAR = System.getProperty("it.localJar", "build/libs/shine-server.jar");
-
- // URI для IT-тестов (переключаем на сервер)
- private static final String WS_URI_SERVER = System.getProperty("it.wsUri", "wss://shineup.me/ws");
-
- public static void main(String[] args) {
-
- // 0) Build shadowJar локально
-// shStrict("./gradlew -q shadowJar");
-
- // 1) stop service на сервере
- sshStrict("sudo systemctl stop " + SERVICE_NAME + " || true");
-
- // 2) upload jar -> .new
- scpStrict(LOCAL_JAR, REMOTE_JAR + ".new");
-
- // 3) заменить jar атомарно
- sshStrict("mv -f " + q(REMOTE_JAR + ".new") + " " + q(REMOTE_JAR));
-
- // 4) удалить data/*
- // (на всякий случай: если папки нет — создать)
- sshStrict("mkdir -p " + q(REMOTE_DATA) + " && rm -rf " + q(REMOTE_DATA) + "/*");
-
- // 5) start service
- sshStrict("sudo systemctl start " + SERVICE_NAME);
-
- // 6) дождаться поднятия (простая проверка: порт слушается)
- waitRemotePort7070();
-
- // 7) переключаем IT на серверный WS URI (без правок исходников)
- System.setProperty("it.wsUri", WS_URI_SERVER);
-
- // 8) прогон тестов
- int failed = IT_RunAllMain.runAll();
- System.exit(failed);
- }
-
- private static void waitRemotePort7070() {
- for (int i = 0; i < 50; i++) {
- int code = ssh("ss -ltnp | grep -q ':7070'"); // 0 если найдено
- if (code == 0) return;
- sleepMs(200);
- }
- throw new RuntimeException("Remote port 7070 did not start in time on " + REMOTE_HOST);
- }
-
- // ---------- helpers ----------
- private static void shStrict(String cmd) {
- int code = sh(cmd);
- if (code != 0) throw new RuntimeException("Command failed (" + code + "): " + cmd);
- }
-
- private static void sshStrict(String remoteCmd) {
- int code = ssh(remoteCmd);
- if (code != 0) throw new RuntimeException("SSH command failed (" + code + "): " + remoteCmd);
- }
-
- private static int ssh(String remoteCmd) {
- String cmd = "ssh " + REMOTE_USER + "@" + REMOTE_HOST + " " + q("bash -lc " + q(remoteCmd));
- return sh(cmd);
- }
-
- private static void scpStrict(String local, String remote) {
- Objects.requireNonNull(local);
- Objects.requireNonNull(remote);
- int code = sh("scp -p " + q(local) + " " + REMOTE_USER + "@" + REMOTE_HOST + ":" + q(remote));
- if (code != 0) throw new RuntimeException("SCP failed (" + code + ")");
- }
-
- private static int sh(String cmd) {
- try {
- Process p = new ProcessBuilder("bash", "-lc", cmd).inheritIO().start();
- return p.waitFor();
- } catch (Exception e) {
- throw new RuntimeException("Command error: " + cmd, e);
- }
- }
-
- private static String q(String s) {
- // простая одинарная кавычка для bash
- return "'" + s.replace("'", "'\"'\"'") + "'";
- }
-
- private static void sleepMs(long ms) {
- try { Thread.sleep(ms); }
- catch (InterruptedException e) { Thread.currentThread().interrupt(); }
- }
-}
-package test.it;
-
-import server.ws.WsServer;
-import test.it.runner.IT_CleanAllDate;
-import test.it.runner.IT_RunAllMain;
-
-public class IT_RunAllCleanStartWsMain {
-
- public static void main(String[] args) {
- runBash("kill -9 $(lsof -t -i:7070) 2>/dev/null || true");
-
- IT_CleanAllDate.main(new String[0]);
-
- Thread wsThread = new Thread(() -> {
- try {
- WsServer.main(new String[0]);
- } catch (Throwable t) {
- t.printStackTrace(System.out);
- }
- }, "wsServer-thread");
- wsThread.setDaemon(true);
- wsThread.start();
-
- sleepMs(1000);
-
- int failed = IT_RunAllMain.runAll();
- System.exit(failed);
- }
-
- private static void runBash(String cmd) {
- try {
- Process p = new ProcessBuilder("bash", "-lc", cmd).inheritIO().start();
- p.waitFor();
- } catch (Exception e) {
- System.out.println("WARN: bash command failed: " + e);
- }
- }
-
- private static void sleepMs(long ms) {
- try {
- Thread.sleep(ms);
- } catch (InterruptedException ignored) {
- Thread.currentThread().interrupt();
- }
- }
-}
-package test.it.runner;
-
-import test.it.utils.TestConfig;
-import test.it.utils.log.TestLog;
-
-import java.io.IOException;
-import java.nio.file.*;
-import java.util.Comparator;
-
-/**
- *
- * Делает:
- * 1) чистит папку data/
- */
-public class IT_CleanAllDate {
-
- private static final String DATA_DIR = "data";
-
- public static void main(String[] args) {
-// ItRunContext.initIfNeeded();
-
- TestLog.title("IT RUN CLEAN: очистка data/ + запуск всех тестов");
-
- try {
- cleanupDataDir(DATA_DIR);
- } catch (Throwable t) {
- TestLog.boom("Не смог очистить data/. Причина: " + t.getMessage());
- if (TestConfig.DEBUG()) t.printStackTrace(System.out);
- System.exit(1);
- }
-
- }
-
- private static void cleanupDataDir(String dirName) throws IOException {
- Path dir = Paths.get(dirName);
-
- if (!Files.exists(dir)) {
- TestLog.warn("data dir not found: " + dir.toAbsolutePath() + " (создаю)");
- Files.createDirectories(dir);
- return;
- }
-
- // удаляем ВСЁ внутри папки, но саму папку оставляем
- Files.walk(dir)
- .sorted(Comparator.reverseOrder())
- .filter(p -> !p.equals(dir))
- .forEach(p -> {
- try {
- Files.deleteIfExists(p);
- } catch (IOException e) {
- throw new RuntimeException("Не смог удалить: " + p.toAbsolutePath(), e);
- }
- });
-
- TestLog.ok("data очищена: " + dir.toAbsolutePath());
- }
-}
-package test.it.runner;
-
-import test.it.cases.IT_01_AddUser;
-import test.it.cases.IT_02_Sessions;
-import test.it.cases.IT_03_AddBlock_NoAuth;
-import test.it.cases.IT_04_UserParams_NoAuth;
-import test.it.cases.IT_05_UserConnections;
-import test.it.utils.log.TestLog;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Ручной запуск всех IT тестов БЕЗ JUnit.
- * Печатает итоги по каждому тесту отдельной строкой.
- */
-public class IT_RunAllMain {
-
- /**
- * Настройка поведения прогона:
- * - true : остановить запуск сразу после первого упавшего теста
- * - false : прогнать все тесты до конца, даже если некоторые упали
- */
- private static final boolean STOP_ON_FIRST_FAIL = true;
-
- public static void main(String[] args) {
- int failed = runAll();
- // при желании можно вернуть код выхода ОС:
- // System.exit(failed == 0 ? 0 : 1);
- }
-
- public static int runAll() {
-
- List summaries = new ArrayList<>();
- int failed = 0;
-
- TestLog.title("IT RUN: запуск всех тестов подряд"
- + (STOP_ON_FIRST_FAIL ? " (STOP_ON_FIRST_FAIL=ON)" : " (STOP_ON_FIRST_FAIL=OFF)"));
-
- String s1 = IT_01_AddUser.run(); summaries.add(s1);
- if (s1.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
-
- String s2 = IT_02_Sessions.run(); summaries.add(s2);
- if (s2.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
-
- String s3 = IT_03_AddBlock_NoAuth.run(); summaries.add(s3);
- if (s3.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
-
- String s4 = IT_04_UserParams_NoAuth.run(); summaries.add(s4);
- if (s4.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
-
- String s5 = IT_05_UserConnections.run(); summaries.add(s5);
- if (s5.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
-
- return finish(summaries, failed);
- }
-
- private static int finishEarly(List summaries, int failed) {
- TestLog.boom("⛔ Остановка прогона: найден FAIL, STOP_ON_FIRST_FAIL=ON");
- return finish(summaries, failed);
- }
-
- private static int finish(List summaries, int failed) {
- TestLog.title("IT RUN RESULT (per test)");
- for (String s : summaries) System.out.println(s);
-
- if (failed == 0) TestLog.ok("\n ВСЕ IT ТЕСТЫ УСПЕШНО ЗАВЕРШЕНЫ");
- else TestLog.boom("❌ IT ПРОГОН УПАЛ: failed=" + failed + " из " + summaries.size());
-
- return failed;
- }
-}
-package test.it.suite;
-
-import org.junit.platform.suite.api.SelectClasses;
-import org.junit.platform.suite.api.Suite;
-import test.it.cases.IT_01_AddUser;
-import test.it.cases.IT_02_Sessions;
-import test.it.cases.IT_03_AddBlock_NoAuth;
-
-/**
- * Сьют, который запускает IT тесты строго в заданном порядке.
- *
- * Запуск:
- * ./gradlew test --tests test.it.suite.IT_00_Suite
- */
-@Suite
-@SelectClasses({
- IT_01_AddUser.class,
- IT_02_Sessions.class,
- IT_03_AddBlock_NoAuth.class
-})
-public class IT_00_Suite {
- // пусто
-}
-package test.it.utils.json;
-
-import test.it.utils.TestIds;
-import test.it.utils.TestConfig;
-import utils.crypto.Ed25519Util;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-
-/** Builder'ы JSON запросов. Внутри автоматически генерим requestId. */
-public final class JsonBuilders {
- private JsonBuilders() {}
-
- // ---------------- AddUser ----------------
-
- public static String addUser(String login) {
- String requestId = TestIds.next("adduser");
- String blockchainName = TestConfig.getBlockchainName(login);
-
- String solanaKeyB64 = TestConfig.solanaPublicKeyB64(login);
- String blockchainKeyB64 = TestConfig.blockchainPublicKeyB64(login);
- String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
-
- return """
- {
- "op": "AddUser",
- "requestId": "%s",
- "payload": {
- "login": "%s",
- "blockchainName": "%s",
- "solanaKey": "%s",
- "blockchainKey": "%s",
- "deviceKey": "%s",
- "bchLimit": %d
- }
- }
- """.formatted(
- requestId,
- login,
- blockchainName,
- solanaKeyB64,
- blockchainKeyB64,
- deviceKeyB64,
- TestConfig.TEST_BCH_LIMIT
- );
- }
-
- // ---------------- GetUser ----------------
-
- public static String getUser(String login) {
- String requestId = TestIds.next("getuser");
- return """
- {
- "op": "GetUser",
- "requestId": "%s",
- "payload": {
- "login": "%s"
- }
- }
- """.formatted(requestId, login);
- }
-
- // ---------------- SearchUsers ----------------
-
- public static String searchUsers(String prefix) {
- String requestId = TestIds.next("searchusers");
- return """
- {
- "op": "SearchUsers",
- "requestId": "%s",
- "payload": {
- "prefix": "%s"
- }
- }
- """.formatted(requestId, prefix);
- }
-
- // ---------------- GetFriendsLists ----------------
-
- public static String getFriendsLists(String login) {
- String requestId = TestIds.next("friends");
- return """
- {
- "op": "GetFriendsLists",
- "requestId": "%s",
- "payload": {
- "login": "%s"
- }
- }
- """.formatted(requestId, login);
- }
-
- // ---------------- AuthChallenge ----------------
-
- public static String authChallenge(String login) {
- String requestId = TestIds.next("auth");
- return """
- {
- "op": "AuthChallenge",
- "requestId": "%s",
- "payload": { "login": "%s" }
- }
- """.formatted(requestId, login);
- }
-
- // ---------------- CreateAuthSession (v2) ----------------
- // v2: sessionKey генерируется/хранится на клиенте, на сервер отправляем sessionPubKeyB64 (base64).
- //
- // ВАЖНО (новое правило):
- // Подпись CreateAuthSession делается ТОЛЬКО deviceKey над строкой:
- // preimage = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce
- //
- // storagePwd и sessionPubKeyB64 НЕ входят в preimage.
-
- public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionPubKeyB64) {
- long timeMs = System.currentTimeMillis();
-
- // подпись делаем devicePrivKey
- byte[] devicePriv = TestConfig.getDevicePrivatKey(login);
- String sigB64 = signAuthCreateSession(login, timeMs, authNonce, devicePriv);
-
- String requestId = TestIds.next("create");
- return """
- {
- "op": "CreateAuthSession",
- "requestId": "%s",
- "payload": {
- "storagePwd": "%s",
- "sessionPubKeyB64": "%s",
- "timeMs": %d,
- "signatureB64": "%s",
- "clientInfo": "%s"
- }
- }
- """.formatted(
- requestId,
- storagePwd,
- sessionPubKeyB64,
- timeMs,
- sigB64,
- TestConfig.TEST_CLIENT_INFO
- );
- }
-
- // ---------------- SessionChallenge (v2) ----------------
-
- public static String sessionChallenge(String sessionId) {
- String requestId = TestIds.next("sch");
- return """
- {
- "op": "SessionChallenge",
- "requestId": "%s",
- "payload": {
- "sessionId": "%s"
- }
- }
- """.formatted(requestId, sessionId);
- }
-
- // ---------------- SessionLogin (v2) ----------------
- // Подпись SessionLogin по-прежнему делается sessionPrivKey:
- // preimage = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce
-
- public static String sessionLogin(String sessionId, String nonce, byte[] sessionPrivKey) {
- long timeMs = System.currentTimeMillis();
- String sigB64 = signSessionLogin(sessionId, timeMs, nonce, sessionPrivKey);
-
- String requestId = TestIds.next("slogin");
- return """
- {
- "op": "SessionLogin",
- "requestId": "%s",
- "payload": {
- "sessionId": "%s",
- "timeMs": %d,
- "signatureB64": "%s",
- "clientInfo": "%s"
- }
- }
- """.formatted(requestId, sessionId, timeMs, sigB64, TestConfig.TEST_CLIENT_INFO);
- }
-
- // ---------------- ListSessions ----------------
-
- public static String listSessions(long timeMs, String signatureB64) {
- String requestId = TestIds.next("list");
- if (signatureB64 == null) signatureB64 = "";
- return """
- {
- "op": "ListSessions",
- "requestId": "%s",
- "payload": {
- }
- }
- """.formatted(requestId, timeMs, signatureB64);
- }
-
- // ---------------- CloseActiveSession ----------------
-
- public static String closeActiveSession(String sessionId, long timeMs, String signatureB64) {
- String requestId = TestIds.next("close");
- if (signatureB64 == null) signatureB64 = "";
- return """
- {
- "op": "CloseActiveSession",
- "requestId": "%s",
- "payload": {
- "sessionId": "%s"
- }
- }
- """.formatted(requestId, sessionId, timeMs, signatureB64);
- }
-
- // ---------------- ListSubscribedChannels ----------------
-
- public static String listSubscribedChannels(String login) {
- String requestId = TestIds.next("subs");
- return """
- {
- "op": "ListSubscribedChannels",
- "requestId": "%s",
- "payload": { "login": "%s" }
- }
- """.formatted(requestId, login);
- }
-
- /**
- * Подпись CreateAuthSession(v2):
- * preimage = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce
- * подписываем devicePrivKey.
- */
- public static String signAuthCreateSession(String login, long timeMs, String authNonce, byte[] devicePrivKey) {
- String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
- byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
- return Base64.getEncoder().encodeToString(sig);
- }
-
- /**
- * Подпись для SessionLogin(v2):
- * preimage = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce
- * подписываем sessionPrivKey.
- */
- public static String signSessionLogin(String sessionId, long timeMs, String nonce, byte[] sessionPrivKey) {
- String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
- byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
- byte[] sig = Ed25519Util.sign(preimage, sessionPrivKey);
- return Base64.getEncoder().encodeToString(sig);
- }
-}
-package test.it.utils.json;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public final class JsonParsers {
- private JsonParsers(){}
- private static final ObjectMapper MAPPER = new ObjectMapper();
-
- public static int status(String json) {
- try {
- JsonNode root = MAPPER.readTree(json);
- return root.has("status") ? root.get("status").asInt() : -1;
- } catch (Exception e) {
- return -1;
- }
- }
-
- public static String authNonce(String json) {
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload != null && payload.has("authNonce")) return payload.get("authNonce").asText();
- return null;
- } catch (Exception e) {
- return null;
- }
- }
-
- /** nonce из SessionChallenge(v2) */
- public static String sessionNonce(String json) {
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload != null && payload.has("nonce")) return payload.get("nonce").asText();
- return null;
- } catch (Exception e) {
- return null;
- }
- }
-
- public static String sessionId(String json) {
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload != null && payload.has("sessionId")) return payload.get("sessionId").asText();
- return null;
- } catch (Exception e) {
- return null;
- }
- }
-
- // оставляю для совместимости с другими тестами, но в IT_02(v2) больше не используется
- public static String sessionPwd(String json) {
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload != null && payload.has("sessionPwd")) return payload.get("sessionPwd").asText();
- return null;
- } catch (Exception e) {
- return null;
- }
- }
-
- public static String storagePwd(String json) {
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload != null && payload.has("storagePwd")) return payload.get("storagePwd").asText();
- return null;
- } catch (Exception e) {
- return null;
- }
- }
-
- public static List sessionIds(String json) {
- List res = new ArrayList<>();
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload == null) return res;
- JsonNode arr = payload.get("sessions");
- if (arr == null || !arr.isArray()) return res;
-
- for (JsonNode s : arr) {
- JsonNode id = s.get("sessionId");
- if (id != null && !id.isNull()) res.add(id.asText());
- }
- } catch (Exception ignored) {}
- return res;
- }
-
- public static String errorCode(String json) {
- try {
- JsonNode root = MAPPER.readTree(json);
-
- // поддержка старого формата (верхний уровень)
- if (root.has("errorCode")) return root.get("errorCode").asText();
- // поддержка нового формата (верхний уровень)
- if (root.has("code")) return root.get("code").asText();
-
- JsonNode payload = root.get("payload");
- if (payload != null) {
- // поддержка старого формата (внутри payload)
- if (payload.has("errorCode")) return payload.get("errorCode").asText();
- // поддержка нового формата (внутри payload)
- if (payload.has("code")) return payload.get("code").asText();
- }
- } catch (Exception ignored) {}
-
- return null;
- }
-
- // ---------------- GetUser helpers ----------------
-
- public static Boolean exists(String json) {
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload != null && payload.has("exists")) return payload.get("exists").asBoolean();
- return null;
- } catch (Exception e) {
- return null;
- }
- }
-
- public static String userLogin(String json) {
- return getPayloadText(json, "login");
- }
-
- public static String userBlockchainName(String json) {
- return getPayloadText(json, "blockchainName");
- }
-
- public static String userSolanaKey(String json) {
- return getPayloadText(json, "solanaKey");
- }
-
- public static String userBlockchainKey(String json) {
- return getPayloadText(json, "blockchainKey");
- }
-
- public static String userDeviceKey(String json) {
- return getPayloadText(json, "deviceKey");
- }
-
- // ---------------- SearchUsers helpers ----------------
-
- public static List searchLogins(String json) {
- List res = new ArrayList<>();
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload == null) return res;
-
- JsonNode arr = payload.get("logins");
- if (arr == null || !arr.isArray()) return res;
-
- for (JsonNode x : arr) {
- if (x != null && !x.isNull()) res.add(x.asText());
- }
- } catch (Exception ignored) {}
- return res;
- }
-
- // ---------------- Friends helpers ----------------
-
- /** payload.login (канонический) */
- public static String friendsLogin(String json) {
- return getPayloadText(json, "login");
- }
-
- public static List friendsOut(String json) {
- return getPayloadStringArray(json, "out_friends");
- }
-
- public static List friendsIn(String json) {
- return getPayloadStringArray(json, "in_friends");
- }
-
- public static List friendsMutual(String json) {
- return getPayloadStringArray(json, "mutual_friends");
- }
-
- private static List getPayloadStringArray(String json, String field) {
- List res = new ArrayList<>();
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload == null) return res;
-
- JsonNode arr = payload.get(field);
- if (arr == null || !arr.isArray()) return res;
-
- for (JsonNode x : arr) {
- if (x != null && !x.isNull()) res.add(x.asText());
- }
- } catch (Exception ignored) {}
- return res;
- }
-
- private static String getPayloadText(String json, String field) {
- try {
- JsonNode root = MAPPER.readTree(json);
- JsonNode payload = root.get("payload");
- if (payload != null && payload.has(field) && !payload.get(field).isNull()) {
- return payload.get(field).asText();
- }
- return null;
- } catch (Exception e) {
- return null;
- }
- }
-}
-package test.it.utils.log;
-
-import test.it.utils.TestConfig;
-
-/**
- * TestLog — единое место для:
- * - ANSI цветов
- * - стандартных сообщений (title/step/send/recv)
- * - PASS/FAIL строк и окраски
- *
- * Режим:
- * - it.debug=false: печатаем минимум (без JSON)
- * - it.debug=true: печатаем JSON отправка/ответ + заголовки шагов
- */
-public final class TestLog {
- private TestLog() {}
-
- public static final boolean DEBUG = TestConfig.DEBUG();
-
- // ANSI COLORS (ТОЛЬКО ТУТ)
- public static final String R = "\u001B[0m";
- public static final String G = "\u001B[32m";
- public static final String Y = "\u001B[33m";
- public static final String RED = "\u001B[31m";
- public static final String C = "\u001B[36m";
-
- public static String green(String s) { return G + s + R; }
- public static String red(String s) { return RED + s + R; }
- public static String cyan(String s) { return C + s + R; }
-
- /** Инфо (печатается только при DEBUG=true). */
- public static void info(String s) {
- if (DEBUG) System.out.println(s);
- }
-
- public static void line() {
- if (!DEBUG) return;
- System.out.println(C + "------------------------------------------------------------" + R);
- }
-
- public static void title(String s) {
- if (!DEBUG) return;
- System.out.println(C + "\n============================================================" + R);
- System.out.println(C + s + R);
- System.out.println(C + "============================================================\n" + R);
- }
-
- public static void titleBlock(String multiLineText) {
- if (!DEBUG) return;
- System.out.println(C + "\n============================================================" + R);
- System.out.println(C + multiLineText + R);
- System.out.println(C + "============================================================\n" + R);
- }
-
- public static void stepTitle(String s) {
- if (!DEBUG) return;
- System.out.println(C + "\n-------------------- " + s + " --------------------" + R);
- }
-
- /** OK (печатаем ВСЕГДА, чтобы было видно зелёное прохождение шагов). */
- public static void ok(String s) {
- System.out.println(G + "✅ " + s + R);
- }
-
- /** WARN (только DEBUG). */
- public static void warn(String s) {
- if (!DEBUG) return;
- System.out.println(Y + "⚠️ " + s + R);
- }
-
- /** FAIL (печатаем ВСЕГДА). */
- public static void boom(String s) {
- System.out.println(RED + "****************************************************************" + R);
- System.out.println(RED + "❌ " + s + R);
- System.out.println(RED + "****************************************************************" + R);
- }
-
- public static void send(String op, String json) {
- if (!DEBUG) return;
- System.out.println("📤 [" + op + "] Request JSON:");
- System.out.println(json);
- line();
- }
-
- public static void recv(String op, String json) {
- if (!DEBUG) return;
- System.out.println("📥 [" + op + "] Response JSON:");
- System.out.println(json);
- line();
- }
-}
-package test.it.utils.log;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * TestResult — накопитель результатов внутри одного теста:
- * - ok(...) печатает зелёным
- * - fail(...) печатает красным и добавляет в итоговую строку
- * - summaryLine() возвращает одну строку: PASS/FAIL + детали
- */
-public final class TestResult {
-
- private final String testName;
- private final List