Полученный кошелёк
Тип—
diff --git a/SHiNE-browser-plugin-wallet/popup.js b/SHiNE-browser-plugin-wallet/popup.js
index 5f55508..10b7572 100644
--- a/SHiNE-browser-plugin-wallet/popup.js
+++ b/SHiNE-browser-plugin-wallet/popup.js
@@ -2,7 +2,6 @@ import { formatPairingShortCode } from './js/lib/device-pairing.js';
const els = {
serverLoginInfo: document.querySelector('#server-login-info'),
- serverAddress: document.querySelector('#server-address'),
loginInput: document.querySelector('#login-input'),
usePassword: document.querySelector('#use-password'),
passwordField: document.querySelector('#password-field'),
@@ -17,9 +16,6 @@ const els = {
status: document.querySelector('#status'),
sessionCard: document.querySelector('#session-card'),
sessionLogin: document.querySelector('#session-login'),
- sessionId: document.querySelector('#session-id'),
- sessionType: document.querySelector('#session-type'),
- deviceKeyShort: document.querySelector('#device-key-short'),
resumeBtn: document.querySelector('#resume-btn'),
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
disconnectBtn: document.querySelector('#disconnect-btn'),
@@ -27,6 +23,10 @@ const els = {
deviceSelect: document.querySelector('#device-select'),
homeserverList: document.querySelector('#homeserver-list'),
requestWalletBtn: document.querySelector('#request-wallet-btn'),
+ pendingApprovalCard: document.querySelector('#pending-approval-card'),
+ pendingApprovalSubtitle: document.querySelector('#pending-approval-subtitle'),
+ pendingApprovalDetails: document.querySelector('#pending-approval-details'),
+ cancelPendingApprovalBtn: document.querySelector('#cancel-pending-approval-btn'),
walletResultCard: document.querySelector('#wallet-result-card'),
walletType: document.querySelector('#wallet-type'),
walletPubkey: document.querySelector('#wallet-pubkey'),
@@ -52,6 +52,7 @@ let state = {
selectedDeviceName: '',
},
currentWallet: null,
+ pendingApproval: null,
status: {
text: '',
kind: 'info',
@@ -107,13 +108,50 @@ function renderHomeserverList(items = []) {
});
}
+function renderPendingApproval(pendingApproval) {
+ els.pendingApprovalDetails.innerHTML = '';
+ if (!pendingApproval) return;
+ const summary = pendingApproval.transactionSummary || {};
+ const programs = Array.isArray(summary.programs) && summary.programs.length
+ ? summary.programs.join(', ')
+ : 'не определены';
+ const details = [
+ { label: 'Сайт', value: pendingApproval.origin || '—', mono: true },
+ { label: 'Кошелёк', value: pendingApproval.publicKeyBase58 || '—', mono: true },
+ { label: 'Очередь', value: `${pendingApproval.queuePosition || 1} из ${pendingApproval.queueLength || 1}` },
+ { label: 'Комментарий', value: pendingApproval.comment || 'Транзакция запрошена сайтом' },
+ { label: 'Тип', value: summary.kind || 'legacy' },
+ { label: 'Инструкций', value: String(summary.instructionCount ?? 0) },
+ { label: 'Программы', value: programs, mono: true },
+ ];
+ if (summary.feePayer) {
+ details.push({ label: 'Fee payer', value: summary.feePayer, mono: true });
+ }
+ if (summary.recentBlockhash) {
+ details.push({ label: 'Blockhash', value: summary.recentBlockhash, mono: true });
+ }
+ for (const item of details) {
+ const row = document.createElement('div');
+ row.className = 'detail-row';
+ const label = document.createElement('div');
+ label.className = 'detail-label';
+ label.textContent = item.label;
+ const value = document.createElement('div');
+ value.className = `detail-value${item.mono ? ' mono' : ''}`;
+ value.textContent = item.value;
+ row.append(label, value);
+ els.pendingApprovalDetails.append(row);
+ }
+}
+
function applyState(nextState) {
state = nextState || state;
const loginValue = String(state?.settings?.login || '');
const resolvedServerLogin = String(state?.settings?.serverLogin || '').trim();
const resolvedServerAddress = String(state?.settings?.serverHttp || '').trim();
- els.serverLoginInfo.textContent = resolvedServerLogin ? `Сервер SHiNE: ${resolvedServerLogin}` : 'Сервер SHiNE: —';
- els.serverAddress.textContent = resolvedServerAddress ? `Адрес: ${resolvedServerAddress}` : 'Адрес: —';
+ els.serverLoginInfo.textContent = resolvedServerLogin && resolvedServerAddress
+ ? `Сервер SHiNE: ${resolvedServerLogin} (${resolvedServerAddress})`
+ : 'Сервер SHiNE: —';
if (document.activeElement !== els.loginInput) {
els.loginInput.value = loginValue;
}
@@ -125,16 +163,15 @@ function applyState(nextState) {
const walletProfile = state?.walletProfile;
const signing = state?.signing || {};
const currentWallet = state?.currentWallet || null;
+ const pendingApproval = state?.pendingApproval || null;
els.connectCard.classList.toggle('hidden', !!session);
els.sessionCard.classList.toggle('hidden', !session);
els.walletCard.classList.toggle('hidden', !session);
+ els.pendingApprovalCard.classList.toggle('hidden', !pendingApproval);
if (session) {
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 || '');
}
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
@@ -149,6 +186,19 @@ function applyState(nextState) {
renderHomeserverList(homeservers);
els.requestWalletBtn.disabled = !session || !signing.selectedDeviceName;
+ if (pendingApproval) {
+ const queueSuffix = (pendingApproval.queueLength || 1) > 1
+ ? ` В очереди ${pendingApproval.queueLength} транзакции.`
+ : '';
+ els.pendingApprovalSubtitle.textContent = pendingApproval.origin
+ ? `Сайт ${pendingApproval.origin} запросил подписание транзакции.${queueSuffix}`
+ : `Сайт запросил подписание транзакции.${queueSuffix}`;
+ renderPendingApproval(pendingApproval);
+ } else {
+ els.pendingApprovalSubtitle.textContent = 'Сайт запросил подписание транзакции.';
+ els.pendingApprovalDetails.innerHTML = '';
+ }
+
if (currentWallet?.publicKeyBase58) {
els.walletResultCard.classList.remove('hidden');
els.walletType.textContent = currentWallet.type || '—';
@@ -313,6 +363,14 @@ async function copyWalletKey() {
}
}
+async function cancelPendingApproval() {
+ try {
+ await sendMessage('wallet:cancelPendingSiteApproval');
+ } catch (error) {
+ setStatus(error.message || 'Не удалось отменить ожидание подписи.', 'error');
+ }
+}
+
function startUiRefreshLoop() {
stopUiRefreshLoop();
refreshTimer = window.setInterval(() => {
@@ -347,6 +405,7 @@ function bindUi() {
els.deviceSelect.addEventListener('change', () => { void updateDeviceSelection(); });
els.requestWalletBtn.addEventListener('click', () => { void requestCurrentWallet(); });
els.copyWalletBtn.addEventListener('click', () => { void copyWalletKey(); });
+ els.cancelPendingApprovalBtn.addEventListener('click', () => { void cancelPendingApproval(); });
}
async function init() {
diff --git a/SHiNE-browser-plugin-wallet/provider-bridge.js b/SHiNE-browser-plugin-wallet/provider-bridge.js
index b19527c..d632234 100644
--- a/SHiNE-browser-plugin-wallet/provider-bridge.js
+++ b/SHiNE-browser-plugin-wallet/provider-bridge.js
@@ -2,6 +2,7 @@ import { PublicKey, Transaction, VersionedTransaction } from './js/lib/vendor/so
const PAGE_REQUEST = 'shine-wallet-page-request';
const PAGE_RESPONSE = 'shine-wallet-page-response';
+const PAGE_MESSAGE_TARGET_ORIGIN = '*';
const STANDARD_REGISTER_EVENT = 'wallet-standard:register-wallet';
const STANDARD_APP_READY_EVENT = 'wallet-standard:app-ready';
const SOLANA_CHAINS = ['solana:mainnet', 'solana:devnet', 'solana:testnet'];
@@ -39,6 +40,45 @@ function createProviderError(message, code = '') {
return error;
}
+function summarizeTransaction(transaction) {
+ const summary = {
+ kind: 'legacy',
+ instructionCount: 0,
+ accountCount: 0,
+ feePayer: '',
+ recentBlockhash: '',
+ programs: [],
+ };
+ if (!transaction) return summary;
+
+ const isVersioned = typeof transaction?.version === 'number' || transaction instanceof VersionedTransaction;
+ summary.kind = isVersioned ? `versioned:${String(transaction.version)}` : 'legacy';
+ summary.feePayer = String(transaction?.feePayer?.toBase58?.() || '').trim();
+ summary.recentBlockhash = String(transaction?.recentBlockhash || transaction?.message?.recentBlockhash || '').trim();
+
+ if (isVersioned) {
+ const message = transaction?.message || {};
+ const staticKeys = Array.isArray(message?.staticAccountKeys) ? message.staticAccountKeys : [];
+ const instructions = Array.isArray(message?.compiledInstructions) ? message.compiledInstructions : [];
+ summary.instructionCount = instructions.length;
+ summary.accountCount = staticKeys.length;
+ summary.programs = instructions
+ .map((instruction) => staticKeys[instruction?.programIdIndex]?.toBase58?.() || '')
+ .filter(Boolean)
+ .slice(0, 5);
+ return summary;
+ }
+
+ const instructions = Array.isArray(transaction?.instructions) ? transaction.instructions : [];
+ summary.instructionCount = instructions.length;
+ summary.accountCount = Array.isArray(transaction?.signatures) ? transaction.signatures.length : 0;
+ summary.programs = instructions
+ .map((instruction) => instruction?.programId?.toBase58?.() || '')
+ .filter(Boolean)
+ .slice(0, 5);
+ return summary;
+}
+
function createRequest(method, params = {}) {
const id = `shine-wallet-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
return new Promise((resolve, reject) => {
@@ -59,7 +99,7 @@ function createRequest(method, params = {}) {
id,
method,
params,
- }, window.location.origin);
+ }, PAGE_MESSAGE_TARGET_ORIGIN);
});
}
@@ -122,12 +162,6 @@ class ShineProviderCore {
async connect(options = {}) {
const onlyIfTrusted = !!options?.onlyIfTrusted || !!options?.silent;
- if (!onlyIfTrusted) {
- const confirmed = window.confirm(`Connect SHiNE Wallet to ${window.location.origin}?`);
- if (!confirmed) {
- throw createProviderError('User rejected wallet connection', 'USER_REJECTED');
- }
- }
const result = await createRequest('connect', { onlyIfTrusted });
const nextKey = new PublicKey(String(result?.publicKeyBase58 || '').trim());
this.publicKey = nextKey;
@@ -157,10 +191,12 @@ class ShineProviderCore {
await this.connect();
}
const transactionBase64 = serializeTransactionBase64(transaction);
+ const transactionSummary = summarizeTransaction(transaction);
const result = await createRequest('signTransaction', {
publicKeyBase58: this.publicKeyBase58,
transactionBase64,
comment: String(comment || '').trim() || `Site ${window.location.origin} requested transaction signature`,
+ transactionSummary,
});
return deserializeSignedTransaction(String(result?.signedTransactionBase64 || ''), transaction);
}
@@ -169,10 +205,20 @@ class ShineProviderCore {
if (!this.publicKey) {
await this.connect();
}
+ const transactionSummary = {
+ kind: 'raw-bytes',
+ instructionCount: 0,
+ accountCount: 0,
+ feePayer: this.publicKeyBase58,
+ recentBlockhash: '',
+ programs: [],
+ byteLength: Number(transactionBytes?.length || 0),
+ };
const result = await createRequest('signTransaction', {
publicKeyBase58: this.publicKeyBase58,
transactionBase64: bytesToBase64(transactionBytes),
comment: String(comment || '').trim() || `Site ${window.location.origin} requested transaction signature`,
+ transactionSummary,
});
return base64ToBytes(String(result?.signedTransactionBase64 || '').trim());
}
@@ -262,6 +308,15 @@ class ShineSolanaProvider {
return this.core.signTransaction(transaction);
}
+ async signAllTransactions(transactions = []) {
+ const list = Array.isArray(transactions) ? transactions : [];
+ const outputs = [];
+ for (const transaction of list) {
+ outputs.push(await this.core.signTransaction(transaction));
+ }
+ return outputs;
+ }
+
async request(args = {}) {
const method = String(args?.method || '');
const params = args?.params;
@@ -275,6 +330,12 @@ class ShineSolanaProvider {
const tx = Array.isArray(params) ? params[0] : params?.transaction || params;
return this.signTransaction(tx);
}
+ if (method === 'signAllTransactions') {
+ const transactions = Array.isArray(params)
+ ? params
+ : Array.isArray(params?.transactions) ? params.transactions : [];
+ return this.signAllTransactions(transactions);
+ }
throw createProviderError(`Unsupported request method: ${method}`, 'UNSUPPORTED_METHOD');
}
}
diff --git a/SHiNE-server/AGENTS.md b/SHiNE-server/AGENTS.md
index 4734e4f..dd8dd5d 100644
--- a/SHiNE-server/AGENTS.md
+++ b/SHiNE-server/AGENTS.md
@@ -42,7 +42,7 @@ shine-UI/server-ui.html
Для обновления — только root + device (blockchain-ключ не нужен).
Актуальные адреса программ Solana (devnet):
-- `shine_users`: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
+- `shine_users`: `3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ`
- `shine_payments`: `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`
Подробнее: `Dev_Docs/Инициализация_Solana_регистрации/README.md`
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-config/src/main/java/utils/config/SolanaProgramsConfig.java b/SHiNE-server/shine-server-config/src/main/java/utils/config/SolanaProgramsConfig.java
index 4d6fdbc..e540c25 100644
--- a/SHiNE-server/shine-server-config/src/main/java/utils/config/SolanaProgramsConfig.java
+++ b/SHiNE-server/shine-server-config/src/main/java/utils/config/SolanaProgramsConfig.java
@@ -12,10 +12,9 @@ public final class SolanaProgramsConfig {
public static final String SOLANA_RPC_URL = "https://api.devnet.solana.com";
// Программа регистрации пользователей (shine_users), задеплоена в devnet.
- public static final String SHINE_USERS_PROGRAM_ID = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm";
+ public static final String SHINE_USERS_PROGRAM_ID = "3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ";
// Отдельно фиксируем адреса связанной инфраструктуры, чтобы UI/сервер ссылались одинаково.
public static final String SHINE_LOGIN_GUARD_PROGRAM_ID = "3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo";
public static final String SHINE_PAYMENTS_PROGRAM_ID = "c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW";
}
-
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..53f65a7 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<>();
@@ -167,10 +167,12 @@ public final class SolanaUserPdaImportService {
int blockVer = u8(raw, c++);
if (blockVer != 0) return null;
- if (blockType == 1) {
+ if (blockType == 0) {
+ c += 32; // recovery_key
+ } else 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 +247,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 +320,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