From ae63a653c8085bb6ad6310f68f4b054b48ba9300d1d4b7e3a08cbc30bc307ebb Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 23 Dec 2025 12:59:12 +0300 Subject: [PATCH] =?UTF-8?q?23=2012=2025=20=D0=9F=D1=80=D0=BE=D1=88=D0=BB?= =?UTF-8?q?=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B5=D1=81?= =?UTF-8?q?=D1=81=D0=B8=D0=B8=20-=20=D0=BF=D0=BE=D1=81=D1=83=D1=82=D0=B8?= =?UTF-8?q?=20=D0=B2=D1=81=D1=91=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=20(=D0=BD=D0=BE=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BF=D0=BE=D0=BA=D0=B0=20=D0=BD=D0=B5=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/blockchain/BchBlockValidator.java | 21 +- .../java/Test/Test_AddBlock_new_NoAuth.java | 644 +++++++++--------- .../Test_AddUser_and_Authorification.java | 2 +- src/main/java/WsTestClient.java | 356 ---------- .../server/logic/InboundMessageProcessor.java | 2 +- .../binary/handlers/AddBlockHandler.java | 500 +++++++------- src/main/java/test/it/WsTestClient.java | 76 +++ src/test/java/test/it/AddUserIT.java | 34 + src/test/java/test/it/JsonBuilders.java | 111 +++ src/test/java/test/it/JsonParsers.java | 82 +++ src/test/java/test/it/SessionsIT.java | 166 +++++ src/test/java/test/it/TestConfig.java | 37 + 12 files changed, 1094 insertions(+), 937 deletions(-) delete mode 100644 src/main/java/WsTestClient.java create mode 100644 src/main/java/test/it/WsTestClient.java create mode 100644 src/test/java/test/it/AddUserIT.java create mode 100644 src/test/java/test/it/JsonBuilders.java create mode 100644 src/test/java/test/it/JsonParsers.java create mode 100644 src/test/java/test/it/SessionsIT.java create mode 100644 src/test/java/test/it/TestConfig.java diff --git a/shine-server-blockchain/src/main/java/blockchain/BchBlockValidator.java b/shine-server-blockchain/src/main/java/blockchain/BchBlockValidator.java index 515c246..07a4179 100644 --- a/shine-server-blockchain/src/main/java/blockchain/BchBlockValidator.java +++ b/shine-server-blockchain/src/main/java/blockchain/BchBlockValidator.java @@ -1,6 +1,5 @@ package blockchain; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import utils.blockchain.BchInfoEntry; @@ -24,11 +23,6 @@ public final class BchBlockValidator { /** * Проверяет, что блок может быть корректно добавлен к цепочке. * - * Не используется при получении запроса на добавление блока по сети (тк там возвращаются более протоколо осмысленные коды - * если блок не подходит по номеру. - * - * А этот класс может быть использован в будущем для внутренних, повторных проверок существующих цепочек блоков. - * * @param block блок (распарсенный из байт) * @param chain информация о цепочке (BchInfoEntry) * @param chainId идентификатор цепочки @@ -51,6 +45,19 @@ public final class BchBlockValidator { return false; } +// // 1.5️⃣ Проверим, что body хотя бы содержит type/ver (первые 4 байта) +// try { +// short bodyType = block.getBodyType(); +// short bodyVer = block.getBodyVer(); +// // тут специально не валидируем смысл (это делает парсер/логика выше), +// // но оставим для диагностики +// log.debug("Body type/ver from bodyBytes: type={} ver={} (blockNum={}, chainId={})", +// bodyType, bodyVer, block.recordNumber, chainId); +// } catch (Exception e) { +// log.warn("❌ Некорректное тело блока: {}", e.getMessage()); +// return false; +// } + // 2️⃣ Проверка публичного ключа byte[] publicKey = chain.getPublicKey32(); if (publicKey == null || publicKey.length != 32) { @@ -100,4 +107,4 @@ public final class BchBlockValidator { } return out; } -} +} \ No newline at end of file diff --git a/src/main/java/Test/Test_AddBlock_new_NoAuth.java b/src/main/java/Test/Test_AddBlock_new_NoAuth.java index eac7190..89b382d 100644 --- a/src/main/java/Test/Test_AddBlock_new_NoAuth.java +++ b/src/main/java/Test/Test_AddBlock_new_NoAuth.java @@ -1,322 +1,322 @@ -package Test; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import utils.crypto.Ed25519Util; -import blockchain.body.HeaderBody; -import blockchain.body.TextBody; -import blockchain_new.BchCryptoVerifier_new; -import blockchain_new.BchBlockEntry_new; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Base64; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; - -public class Test_AddBlock_new_NoAuth { - - private static final String WS_URI = "ws://localhost:7070/ws"; - private static final ObjectMapper JSON = new ObjectMapper(); - - private static final String TEST_LOGIN = "anya24"; - // По твоему правилу: blockchainName = login + 4 цифры - private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; - - private static final byte[] LOGIN_PRIV_KEY; - private static final byte[] LOGIN_PUB_KEY; - - static { - LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN); - LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY); - } - - private static final byte[] ZERO32 = new byte[32]; - private static final String ZERO64 = "0".repeat(64); - - public static void main(String[] args) throws Exception { - CountDownLatch latch = new CountDownLatch(1); - HttpClient client = HttpClient.newHttpClient(); - - client.newWebSocketBuilder() - .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { - - private int step = 0; - - // Эти значения обновим ПО ОТВЕТУ сервера на header - private String lastGlobalHashHex = ZERO64; - private String lastLineHashHex = ZERO64; - - @Override - public void onOpen(WebSocket ws) { - System.out.println("✅ WS connected: " + WS_URI); - ws.request(1); - - // 1) HEADER (global=0, line=0, lineNumber=0) - byte[] headerFull = buildHeaderBlockFullBytes( - /*global*/0, - /*lineIndex*/(short)0, - /*lineBlock*/0, - /*prevGlobal*/ZERO32, - /*prevLine*/ZERO32 - ); - - String json = buildAddBlockJson( - "test-add-header", - TEST_BCH_NAME, - 0, - ZERO64, // prevGlobalHash для первого блока — нули - base64(headerFull) - ); - - System.out.println("\n📤 SEND #1 (HEADER):\n" + json); - ws.sendText(json, true); - } - - @Override - public CompletionStage onText(WebSocket ws, CharSequence data, boolean last) { - String msg = data.toString(); - System.out.println("\n📥 RECV:\n" + msg); - System.out.println("-----------------------------------------------------"); - - try { - int status = extractStatus(msg); - - if (step == 0) { - if (status != 200) { - System.out.println("❌ HEADER rejected, status=" + status); - ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail"); - return CompletableFuture.completedFuture(null); - } - - // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state - String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash"); - String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash"); - - if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) { - System.out.println("❌ No serverLastGlobalHash in response"); - ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response"); - return CompletableFuture.completedFuture(null); - } - if (serverLastLineHash == null || serverLastLineHash.isBlank()) { - // fallback: пусть будет как global (если сервер так хранит) - serverLastLineHash = serverLastGlobalHash; - } - - lastGlobalHashHex = serverLastGlobalHash; - lastLineHashHex = serverLastLineHash; - - byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex); - byte[] prevLine32 = hexToBytes32(lastLineHashHex); - - // 2) TEXT (global=1, line=0, lineNumber=1) - byte[] textFull = buildTextBlockFullBytes( - /*global*/1, - /*lineIndex*/(short)0, - /*lineBlock*/1, - prevGlobal32, - prevLine32, - "Hello from test client" - ); - - String json2 = buildAddBlockJson( - "test-add-text", - TEST_BCH_NAME, - 1, - lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера - base64(textFull) - ); - - System.out.println("\n📤 SEND #2 (TEXT):\n" + json2); - step = 1; - ws.sendText(json2, true); - - } else if (step == 1) { - if (status != 200) { - System.out.println("❌ TEXT rejected, status=" + status); - } else { - System.out.println("✅ Done. Closing."); - } - ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok"); - } - - } catch (Exception e) { - e.printStackTrace(System.out); - ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception"); - } - - ws.request(1); - return CompletableFuture.completedFuture(null); - } - - @Override - public void onError(WebSocket ws, Throwable error) { - System.out.println("❌ WS error: " + error.getMessage()); - error.printStackTrace(System.out); - latch.countDown(); - } - - @Override - public CompletionStage onClose(WebSocket ws, int statusCode, String reason) { - System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason); - latch.countDown(); - return CompletableFuture.completedFuture(null); - } - }).join(); - - latch.await(); - } - - // ================================================================================= - // BUILD BLOCKS - // ================================================================================= - - private static byte[] buildHeaderBlockFullBytes(int globalNumber, - short lineIndex, - int lineBlockNumber, - byte[] prevGlobalHash32, - byte[] prevLineHash32) { - - HeaderBody body = new HeaderBody( - TEST_BCH_NAME, // было TEST_BCH_ID (long), теперь имя блокчейна (String) - TEST_LOGIN, - 0, 0, - (short) 1, - 0L, - LOGIN_PUB_KEY - ); - byte[] bodyBytes = body.toBytes(); - - return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); - } - - private static byte[] buildTextBlockFullBytes(int globalNumber, - short lineIndex, - int lineBlockNumber, - byte[] prevGlobalHash32, - byte[] prevLineHash32, - String text) { - - TextBody body = new TextBody(text); - byte[] bodyBytes = body.toBytes(); - - return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); - } - - private static byte[] buildSignedBlockFullBytes(int globalNumber, - short lineIndex, - int lineBlockNumber, - byte[] bodyBytes, - byte[] prevGlobalHash32, - byte[] prevLineHash32) { - - long ts = System.currentTimeMillis() / 1000L; - - int recordSize = - BchBlockEntry_new.RAW_HEADER_SIZE + - bodyBytes.length + - BchBlockEntry_new.SIGNATURE_LEN + - BchBlockEntry_new.HASH_LEN; - - byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry_new.RAW_HEADER_SIZE + bodyBytes.length) - .order(ByteOrder.BIG_ENDIAN) - .putInt(recordSize) - .putInt(globalNumber) - .putLong(ts) - .putShort(lineIndex) - .putInt(lineBlockNumber) - .put(bodyBytes) - .array(); - - byte[] preimage = BchCryptoVerifier_new.buildPreimage( - TEST_LOGIN, - prevGlobalHash32, - prevLineHash32, - rawBytes - ); - - byte[] hash32 = BchCryptoVerifier_new.sha256(preimage); - - // если у тебя подпись должна быть по preimage — меняй тут - byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY); - - return new BchBlockEntry_new( - globalNumber, - ts, - lineIndex, - lineBlockNumber, - bodyBytes, - signature64, - hash32 - ).toBytes(); - } - - // ================================================================================= - // JSON BUILD - // ================================================================================= - - private static String buildAddBlockJson(String requestId, - String blockchainName, - int globalNumber, - String prevGlobalHashHex, - String blockBytesB64) { - return """ - { - "op": "AddBlock", - "requestId": "%s", - "payload": { - "login": "%s", - "blockchainName": "%s", - "globalNumber": %d, - "prevGlobalHash": "%s", - "blockBytesB64": "%s" - } - } - """.formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); - } - - // ================================================================================= - // HELPERS - // ================================================================================= - - private static int extractStatus(String json) { - try { - JsonNode root = JSON.readTree(json); - if (root.has("status")) return root.get("status").asInt(); - } catch (Exception ignore) {} - return -1; - } - - private static String extractPayloadString(String json, String field) { - try { - JsonNode root = JSON.readTree(json); - JsonNode payload = root.get("payload"); - if (payload != null && payload.has(field)) { - return payload.get(field).asText(); - } - } catch (Exception ignore) {} - return null; - } - - private static String base64(byte[] bytes) { - return Base64.getEncoder().encodeToString(bytes); - } - - private static byte[] hexToBytes32(String hex) { - if (hex == null) throw new IllegalArgumentException("hex is null"); - String s = hex.trim(); - if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length()); - byte[] out = new byte[32]; - for (int i = 0; i < 32; i++) { - int hi = Character.digit(s.charAt(i * 2), 16); - int lo = Character.digit(s.charAt(i * 2 + 1), 16); - if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2)); - out[i] = (byte) ((hi << 4) | lo); - } - return out; - } -} \ No newline at end of file +//package Test; +// +//import com.fasterxml.jackson.databind.JsonNode; +//import com.fasterxml.jackson.databind.ObjectMapper; +//import utils.crypto.Ed25519Util; +//import blockchain.body.HeaderBody; +//import blockchain.body.TextBody; +//import blockchain_new.BchCryptoVerifier_new; +//import blockchain_new.BchBlockEntry_new; +// +//import java.net.URI; +//import java.net.http.HttpClient; +//import java.net.http.WebSocket; +//import java.nio.ByteBuffer; +//import java.nio.ByteOrder; +//import java.util.Base64; +//import java.util.concurrent.CompletableFuture; +//import java.util.concurrent.CompletionStage; +//import java.util.concurrent.CountDownLatch; +// +//public class Test_AddBlock_new_NoAuth { +// +// private static final String WS_URI = "ws://localhost:7070/ws"; +// private static final ObjectMapper JSON = new ObjectMapper(); +// +// private static final String TEST_LOGIN = "anya24"; +// // По твоему правилу: blockchainName = login + 4 цифры +// private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; +// +// private static final byte[] LOGIN_PRIV_KEY; +// private static final byte[] LOGIN_PUB_KEY; +// +// static { +// LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN); +// LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY); +// } +// +// private static final byte[] ZERO32 = new byte[32]; +// private static final String ZERO64 = "0".repeat(64); +// +// public static void main(String[] args) throws Exception { +// CountDownLatch latch = new CountDownLatch(1); +// HttpClient client = HttpClient.newHttpClient(); +// +// client.newWebSocketBuilder() +// .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { +// +// private int step = 0; +// +// // Эти значения обновим ПО ОТВЕТУ сервера на header +// private String lastGlobalHashHex = ZERO64; +// private String lastLineHashHex = ZERO64; +// +// @Override +// public void onOpen(WebSocket ws) { +// System.out.println("✅ WS connected: " + WS_URI); +// ws.request(1); +// +// // 1) HEADER (global=0, line=0, lineNumber=0) +// byte[] headerFull = buildHeaderBlockFullBytes( +// /*global*/0, +// /*lineIndex*/(short)0, +// /*lineBlock*/0, +// /*prevGlobal*/ZERO32, +// /*prevLine*/ZERO32 +// ); +// +// String json = buildAddBlockJson( +// "test-add-header", +// TEST_BCH_NAME, +// 0, +// ZERO64, // prevGlobalHash для первого блока — нули +// base64(headerFull) +// ); +// +// System.out.println("\n📤 SEND #1 (HEADER):\n" + json); +// ws.sendText(json, true); +// } +// +// @Override +// public CompletionStage onText(WebSocket ws, CharSequence data, boolean last) { +// String msg = data.toString(); +// System.out.println("\n📥 RECV:\n" + msg); +// System.out.println("-----------------------------------------------------"); +// +// try { +// int status = extractStatus(msg); +// +// if (step == 0) { +// if (status != 200) { +// System.out.println("❌ HEADER rejected, status=" + status); +// ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail"); +// return CompletableFuture.completedFuture(null); +// } +// +// // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state +// String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash"); +// String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash"); +// +// if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) { +// System.out.println("❌ No serverLastGlobalHash in response"); +// ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response"); +// return CompletableFuture.completedFuture(null); +// } +// if (serverLastLineHash == null || serverLastLineHash.isBlank()) { +// // fallback: пусть будет как global (если сервер так хранит) +// serverLastLineHash = serverLastGlobalHash; +// } +// +// lastGlobalHashHex = serverLastGlobalHash; +// lastLineHashHex = serverLastLineHash; +// +// byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex); +// byte[] prevLine32 = hexToBytes32(lastLineHashHex); +// +// // 2) TEXT (global=1, line=0, lineNumber=1) +// byte[] textFull = buildTextBlockFullBytes( +// /*global*/1, +// /*lineIndex*/(short)0, +// /*lineBlock*/1, +// prevGlobal32, +// prevLine32, +// "Hello from test client" +// ); +// +// String json2 = buildAddBlockJson( +// "test-add-text", +// TEST_BCH_NAME, +// 1, +// lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера +// base64(textFull) +// ); +// +// System.out.println("\n📤 SEND #2 (TEXT):\n" + json2); +// step = 1; +// ws.sendText(json2, true); +// +// } else if (step == 1) { +// if (status != 200) { +// System.out.println("❌ TEXT rejected, status=" + status); +// } else { +// System.out.println("✅ Done. Closing."); +// } +// ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok"); +// } +// +// } catch (Exception e) { +// e.printStackTrace(System.out); +// ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception"); +// } +// +// ws.request(1); +// return CompletableFuture.completedFuture(null); +// } +// +// @Override +// public void onError(WebSocket ws, Throwable error) { +// System.out.println("❌ WS error: " + error.getMessage()); +// error.printStackTrace(System.out); +// latch.countDown(); +// } +// +// @Override +// public CompletionStage onClose(WebSocket ws, int statusCode, String reason) { +// System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason); +// latch.countDown(); +// return CompletableFuture.completedFuture(null); +// } +// }).join(); +// +// latch.await(); +// } +// +// // ================================================================================= +// // BUILD BLOCKS +// // ================================================================================= +// +// private static byte[] buildHeaderBlockFullBytes(int globalNumber, +// short lineIndex, +// int lineBlockNumber, +// byte[] prevGlobalHash32, +// byte[] prevLineHash32) { +// +// HeaderBody body = new HeaderBody( +// TEST_BCH_NAME, // было TEST_BCH_ID (long), теперь имя блокчейна (String) +// TEST_LOGIN, +// 0, 0, +// (short) 1, +// 0L, +// LOGIN_PUB_KEY +// ); +// byte[] bodyBytes = body.toBytes(); +// +// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); +// } +// +// private static byte[] buildTextBlockFullBytes(int globalNumber, +// short lineIndex, +// int lineBlockNumber, +// byte[] prevGlobalHash32, +// byte[] prevLineHash32, +// String text) { +// +// TextBody body = new TextBody(text); +// byte[] bodyBytes = body.toBytes(); +// +// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); +// } +// +// private static byte[] buildSignedBlockFullBytes(int globalNumber, +// short lineIndex, +// int lineBlockNumber, +// byte[] bodyBytes, +// byte[] prevGlobalHash32, +// byte[] prevLineHash32) { +// +// long ts = System.currentTimeMillis() / 1000L; +// +// int recordSize = +// BchBlockEntry_new.RAW_HEADER_SIZE + +// bodyBytes.length + +// BchBlockEntry_new.SIGNATURE_LEN + +// BchBlockEntry_new.HASH_LEN; +// +// byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry_new.RAW_HEADER_SIZE + bodyBytes.length) +// .order(ByteOrder.BIG_ENDIAN) +// .putInt(recordSize) +// .putInt(globalNumber) +// .putLong(ts) +// .putShort(lineIndex) +// .putInt(lineBlockNumber) +// .put(bodyBytes) +// .array(); +// +// byte[] preimage = BchCryptoVerifier_new.buildPreimage( +// TEST_LOGIN, +// prevGlobalHash32, +// prevLineHash32, +// rawBytes +// ); +// +// byte[] hash32 = BchCryptoVerifier_new.sha256(preimage); +// +// // если у тебя подпись должна быть по preimage — меняй тут +// byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY); +// +// return new BchBlockEntry_new( +// globalNumber, +// ts, +// lineIndex, +// lineBlockNumber, +// bodyBytes, +// signature64, +// hash32 +// ).toBytes(); +// } +// +// // ================================================================================= +// // JSON BUILD +// // ================================================================================= +// +// private static String buildAddBlockJson(String requestId, +// String blockchainName, +// int globalNumber, +// String prevGlobalHashHex, +// String blockBytesB64) { +// return """ +// { +// "op": "AddBlock", +// "requestId": "%s", +// "payload": { +// "login": "%s", +// "blockchainName": "%s", +// "globalNumber": %d, +// "prevGlobalHash": "%s", +// "blockBytesB64": "%s" +// } +// } +// """.formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); +// } +// +// // ================================================================================= +// // HELPERS +// // ================================================================================= +// +// private static int extractStatus(String json) { +// try { +// JsonNode root = JSON.readTree(json); +// if (root.has("status")) return root.get("status").asInt(); +// } catch (Exception ignore) {} +// return -1; +// } +// +// private static String extractPayloadString(String json, String field) { +// try { +// JsonNode root = JSON.readTree(json); +// JsonNode payload = root.get("payload"); +// if (payload != null && payload.has(field)) { +// return payload.get(field).asText(); +// } +// } catch (Exception ignore) {} +// return null; +// } +// +// private static String base64(byte[] bytes) { +// return Base64.getEncoder().encodeToString(bytes); +// } +// +// private static byte[] hexToBytes32(String hex) { +// if (hex == null) throw new IllegalArgumentException("hex is null"); +// String s = hex.trim(); +// if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length()); +// byte[] out = new byte[32]; +// for (int i = 0; i < 32; i++) { +// int hi = Character.digit(s.charAt(i * 2), 16); +// int lo = Character.digit(s.charAt(i * 2 + 1), 16); +// if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2)); +// out[i] = (byte) ((hi << 4) | lo); +// } +// return out; +// } +//} diff --git a/src/main/java/Test/Test_AddUser_and_Authorification.java b/src/main/java/Test/Test_AddUser_and_Authorification.java index c88f7c2..4fde1cb 100644 --- a/src/main/java/Test/Test_AddUser_and_Authorification.java +++ b/src/main/java/Test/Test_AddUser_and_Authorification.java @@ -760,7 +760,7 @@ public class Test_AddUser_and_Authorification { "requestId": "test-add-1", "payload": { "login": "%s", - "bchName": "%s", + "blockchainName": "%s", "loginKey": "%s", "deviceKey": "%s", "bchLimit": %d diff --git a/src/main/java/WsTestClient.java b/src/main/java/WsTestClient.java deleted file mode 100644 index 76a3d89..0000000 --- a/src/main/java/WsTestClient.java +++ /dev/null @@ -1,356 +0,0 @@ -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.*; - -public class WsTestClient { - - // ==== Настройки клиента ==== - static final String WS_URL = "wss://shineup.me/ws";// "ws://localhost:8080/ws"; - - // ==== Тестовые параметры ==== - static final String FIXED_PRIVATE_KEY_STRING = "SHiNE_TEST_FIXED_PRIVATE_KEY_2025"; - static final long BLOCKCHAIN_ID = 351130785469109974L;//777_000_001L; - static final int BLOCKCHAIN_TYPE = 0; - static final int BLOCKCHAIN_NUM = 0; - static final short VERSION_USER_BCH = 1; - static final long PREV_USER_BCH_ID = 0L; - static final String USER_LOGIN = "test_user"; - - // ==== Опкоды ==== - static final int OP_ADD_BLOCK = 1; - static final int OP_GET_BLOCKCHAIN = 2; - - // ==== Статусы ==== - static final int STATUS_OK = 200; - static final int STATUS_BAD_REQUEST = 400; - static final int STATUS_ALREADY_EXISTS = 409; - static final int STATUS_NON_SEQUENTIAL = 412; - static final int STATUS_UNVERIFIED = 422; - static final int STATUS_INTERNAL = 500; - - // ==== Типы блоков ==== - static final short TYPE_HEADER = 0; - static final short TYPE_TEXT = 1; - static final short RECORD_TYPE_VERSION = 1; // Новое поле - - // ==== Константы формата ==== - static final int SIGNATURE_LEN = 64; - static final int HASH_LEN = 32; - static final int RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 2; // Теперь 20 байт - - public static void main(String[] args) throws Exception { - System.out.println("=== WsTestClient v1.1 ==="); - - byte[] priv32 = HashUtil.sha256(FIXED_PRIVATE_KEY_STRING.getBytes(StandardCharsets.UTF_8)); - byte[] pub32 = Ed25519Util.derivePublicKey(priv32); - - WsBinaryCollector reader = new WsBinaryCollector(); - WebSocket ws = HttpClient.newHttpClient() - .newWebSocketBuilder() - .buildAsync(URI.create(WS_URL), reader) - .join(); - System.out.println("✅ Connected to " + WS_URL); - - // === 1. Создание заглавного блока === - byte[] headerBody = buildHeaderBody(USER_LOGIN, BLOCKCHAIN_ID, BLOCKCHAIN_TYPE, BLOCKCHAIN_NUM, - VERSION_USER_BCH, PREV_USER_BCH_ID, pub32); - - long ts = Instant.now().getEpochSecond(); - byte[] rawHeader = buildRawRecord(0, ts, TYPE_HEADER, RECORD_TYPE_VERSION, headerBody); - byte[] fullHeader = signAndPack(rawHeader, USER_LOGIN, BLOCKCHAIN_ID, new byte[32], priv32, pub32); - - byte[] addHeaderMsg = concat(beInt(OP_ADD_BLOCK), beLong(BLOCKCHAIN_ID), fullHeader); - int st1 = sendAndReadStatus(ws, addHeaderMsg, reader); - System.out.println("ADD HEADER → " + st1 + " (" + statusName(st1) + ")"); - - // === 2. Получаем всю цепочку === - ResponseWithPayload chainResp = sendAndReadPayload(ws, concat(beInt(OP_GET_BLOCKCHAIN), beLong(BLOCKCHAIN_ID)), reader); - System.out.println("GET_BLOCKCHAIN → " + chainResp.status + " (" + statusName(chainResp.status) + ")"); - if (chainResp.status != STATUS_OK) return; - - List blocks = parseAllBlocks(chainResp.payload); - System.out.println("Chain contains " + blocks.size() + " blocks:"); - - for (BlockParsed bp : blocks) { - printBlock(bp); - } - - // === 3. Добавление нового текстового блока === - Scanner sc = new Scanner(System.in, StandardCharsets.UTF_8); - System.out.print("\nВведите текст для добавления в блокчейн (Enter — пропустить): "); - String text = sc.nextLine().trim(); - if (!text.isEmpty()) { - byte[] lastHash = blocks.isEmpty() ? new byte[32] : blocks.get(blocks.size() - 1).hash32; - int nextNum = blocks.isEmpty() ? 0 : (blocks.get(blocks.size() - 1).recordNumber + 1); - - byte[] textBody = text.getBytes(StandardCharsets.UTF_8); - byte[] rawText = buildRawRecord(nextNum, Instant.now().getEpochSecond(), TYPE_TEXT, RECORD_TYPE_VERSION, textBody); - byte[] fullText = signAndPack(rawText, USER_LOGIN, BLOCKCHAIN_ID, lastHash, priv32, pub32); - - int st2 = sendAndReadStatus(ws, concat(beInt(OP_ADD_BLOCK), beLong(BLOCKCHAIN_ID), fullText), reader); - System.out.println("ADD TEXT → " + st2 + " (" + statusName(st2) + ")"); - } - - ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").join(); - System.out.println("=== Done ==="); - } - - // ============================================================== - // БЛОКИ - // ============================================================== - - static byte[] buildRawRecord(int recordNumber, long timestampSec, - short recordType, short recordTypeVersion, byte[] body) { - int recordSize = RAW_HEADER_SIZE + body.length; - ByteBuffer buf = ByteBuffer.allocate(recordSize).order(ByteOrder.BIG_ENDIAN); - buf.putInt(recordSize); - buf.putInt(recordNumber); - buf.putLong(timestampSec); - buf.putShort(recordType); - buf.putShort(recordTypeVersion); - buf.put(body); - return buf.array(); - } - - static byte[] buildHeaderBody(String userLogin, long blockchainId, int blockchainType, - int blockchainNumber, short versionUserBch, - long prevUserBchId, byte[] publicKey32) { - byte[] tag = "SHiNE001".getBytes(StandardCharsets.US_ASCII); - byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8); - if (loginUtf8.length > 255) throw new IllegalArgumentException("Логин слишком длинный"); - - int cap = 8 + 8 + 1 + loginUtf8.length + 4 + 4 + 2 + 8 + 32; - ByteBuffer buf = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); - buf.put(tag); - buf.putLong(blockchainId); - buf.put((byte) loginUtf8.length); - buf.put(loginUtf8); - buf.putInt(blockchainType); - buf.putInt(blockchainNumber); - buf.putShort(versionUserBch); - buf.putLong(prevUserBchId); - buf.put(publicKey32); - return buf.array(); - } - - static byte[] signAndPack(byte[] rawBytes, String userLogin, long blockchainId, - byte[] prevHash32, byte[] privateKey32, byte[] publicKey32) { - byte[] preimage = buildPreimage(userLogin, blockchainId, prevHash32, rawBytes); - byte[] hash32 = HashUtil.sha256(preimage); - byte[] sig64 = Ed25519Util.sign(preimage, privateKey32); - return concat(rawBytes, sig64, hash32); - } - - // ============================================================== - // ПАРСИНГ - // ============================================================== - - static class BlockParsed { - int recordSize; - int recordNumber; - long timestamp; - short recordType; - short recordTypeVersion; - byte[] body; - byte[] signature64; - byte[] hash32; - } - - static List parseAllBlocks(byte[] file) { - List out = new ArrayList<>(); - int p = 0; - while (p + 4 <= file.length) { - int recordSize = beInt(file, p); - int total = recordSize + SIGNATURE_LEN + HASH_LEN; - if (p + total > file.length) break; - - ByteBuffer raw = ByteBuffer.wrap(file, p, recordSize).order(ByteOrder.BIG_ENDIAN); - BlockParsed bp = new BlockParsed(); - bp.recordSize = raw.getInt(); - bp.recordNumber = raw.getInt(); - bp.timestamp = raw.getLong(); - bp.recordType = raw.getShort(); - bp.recordTypeVersion = raw.getShort(); - int bodyLen = bp.recordSize - RAW_HEADER_SIZE; - bp.body = new byte[bodyLen]; - raw.get(bp.body); - bp.signature64 = Arrays.copyOfRange(file, p + recordSize, p + recordSize + SIGNATURE_LEN); - bp.hash32 = Arrays.copyOfRange(file, p + recordSize + SIGNATURE_LEN, p + recordSize + SIGNATURE_LEN + HASH_LEN); - out.add(bp); - p += total; - } - return out; - } - - static void printBlock(BlockParsed b) { - System.out.println("------------------------------------------------------------"); - String ts = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()) - .format(Instant.ofEpochSecond(b.timestamp)); - System.out.printf("num=%d, type=%d, ver=%d, ts=%s, size=%d%n", - b.recordNumber, b.recordType, b.recordTypeVersion, ts, b.recordSize); - - if (b.recordType == TYPE_HEADER) - printHeaderBody(b.body); - else if (b.recordType == TYPE_TEXT) - System.out.println("TEXT: " + new String(b.body, StandardCharsets.UTF_8)); - else - System.out.println("UNKNOWN BODY (" + b.body.length + " bytes)"); - - System.out.println("hash=" + toHex(b.hash32)); - } - - static void printHeaderBody(byte[] body) { - ByteBuffer buf = ByteBuffer.wrap(body).order(ByteOrder.BIG_ENDIAN); - byte[] tag = new byte[8]; buf.get(tag); - long id = buf.getLong(); - int n = Byte.toUnsignedInt(buf.get()); - byte[] login = new byte[n]; buf.get(login); - int type = buf.getInt(); - int num = buf.getInt(); - buf.getShort(); buf.getLong(); // version + prev - byte[] pub = new byte[32]; buf.get(pub); - - System.out.println("HEADER: login=" + new String(login, StandardCharsets.UTF_8) + - ", id=" + id + ", type=" + type + ", num=" + num); - System.out.println("(pubkey first 4 bytes: " + toHex(Arrays.copyOf(pub, 4)) + "...)"); - } - - // ============================================================== - // Вебсокет и вспомогательные классы - // ============================================================== - - static int sendAndReadStatus(WebSocket ws, byte[] payload, WsBinaryCollector reader) { - ws.sendBinary(ByteBuffer.wrap(payload), true).join(); - byte[] resp = reader.collect(ws); - if (resp == null || resp.length < 4) throw new IllegalStateException("empty response"); - return beInt(resp, 0); - } - - static class ResponseWithPayload { - int status; - byte[] payload; - } - - static ResponseWithPayload sendAndReadPayload(WebSocket ws, byte[] payload, WsBinaryCollector reader) { - ws.sendBinary(ByteBuffer.wrap(payload), true).join(); - byte[] resp = reader.collect(ws); - ResponseWithPayload out = new ResponseWithPayload(); - out.status = beInt(resp, 0); - if (out.status == STATUS_OK) { - int len = beInt(resp, 4); - out.payload = Arrays.copyOfRange(resp, 8, 8 + len); - } - return out; - } - - static class WsBinaryCollector implements WebSocket.Listener { - private volatile CompletableFuture future = new CompletableFuture<>(); - private ByteBuffer acc = ByteBuffer.allocate(0); - - public synchronized byte[] collect(WebSocket ws) { - acc = ByteBuffer.allocate(0); - future = new CompletableFuture<>(); - ws.request(1); - return future.join(); - } - - @Override public void onOpen(WebSocket ws) { ws.request(1); } - @Override public CompletionStage onBinary(WebSocket ws, ByteBuffer data, boolean last) { - ByteBuffer newBuf = ByteBuffer.allocate(acc.remaining() + data.remaining()); - newBuf.put(acc); newBuf.put(data); newBuf.flip(); - acc = newBuf; - if (last) { - byte[] all = new byte[acc.remaining()]; - acc.get(all); - future.complete(all); - } - ws.request(1); - return CompletableFuture.completedFuture(null); - } - @Override public CompletionStage onText(WebSocket ws, CharSequence data, boolean last) { - if (last) future.complete(data.toString().getBytes(StandardCharsets.UTF_8)); - ws.request(1); - return CompletableFuture.completedFuture(null); - } - @Override public void onError(WebSocket ws, Throwable error) { future.completeExceptionally(error); } - } - - // ============================================================== - // Крипто и утилиты - // ============================================================== - - static byte[] buildPreimage(String userLogin, long blockchainId, byte[] prevHash32, byte[] rawBytes) { - byte[] loginUtf8 = userLogin.getBytes(StandardCharsets.UTF_8); - ByteBuffer buf = ByteBuffer.allocate(loginUtf8.length + 8 + 32 + rawBytes.length).order(ByteOrder.BIG_ENDIAN); - buf.put(loginUtf8); - buf.putLong(blockchainId); - buf.put(prevHash32); - buf.put(rawBytes); - return buf.array(); - } - - static final class HashUtil { - static byte[] sha256(byte[] data) { - org.bouncycastle.crypto.digests.SHA256Digest d = new org.bouncycastle.crypto.digests.SHA256Digest(); - d.update(data, 0, data.length); - byte[] out = new byte[32]; - d.doFinal(out, 0); - return out; - } - } - - static final class Ed25519Util { - static byte[] derivePublicKey(byte[] privateKey32) { - var priv = new org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters(privateKey32, 0); - return priv.generatePublicKey().getEncoded(); - } - static byte[] sign(byte[] data, byte[] privateKey32) { - var priv = new org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters(privateKey32, 0); - var signer = new org.bouncycastle.crypto.signers.Ed25519Signer(); - signer.init(true, priv); - signer.update(data, 0, data.length); - return signer.generateSignature(); - } - } - - // ==== Утилиты ==== - static byte[] concat(byte[]... parts) { - int n = Arrays.stream(parts).mapToInt(a -> a.length).sum(); - byte[] out = new byte[n]; - int off = 0; - for (byte[] p : parts) { System.arraycopy(p, 0, out, off, p.length); off += p.length; } - return out; - } - - static byte[] beInt(int v) { return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(v).array(); } - static byte[] beLong(long v) { return ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putLong(v).array(); } - static int beInt(byte[] a, int off) { return ByteBuffer.wrap(a, off, 4).order(ByteOrder.BIG_ENDIAN).getInt(); } - - static String toHex(byte[] b) { - StringBuilder sb = new StringBuilder(b.length * 2); - for (byte x : b) sb.append(String.format("%02x", x)); - return sb.toString(); - } - - static String statusName(int code) { - return switch (code) { - case STATUS_OK -> "OK"; - case STATUS_BAD_REQUEST -> "BAD_REQUEST"; - case STATUS_ALREADY_EXISTS -> "ALREADY_EXISTS"; - case STATUS_NON_SEQUENTIAL -> "NON_SEQUENTIAL"; - case STATUS_UNVERIFIED -> "UNVERIFIED"; - case STATUS_INTERNAL -> "INTERNAL_ERROR"; - default -> "UNKNOWN"; - }; - } -} - diff --git a/src/main/java/server/logic/InboundMessageProcessor.java b/src/main/java/server/logic/InboundMessageProcessor.java index 49fc13f..be2e404 100644 --- a/src/main/java/server/logic/InboundMessageProcessor.java +++ b/src/main/java/server/logic/InboundMessageProcessor.java @@ -19,7 +19,7 @@ public final class InboundMessageProcessor { private static final Map HANDLERS = Map.of( WireCodes.Op.PING, new PingHandler(), - WireCodes.Op.ADD_BLOCK, new AddBlockHandler(), +// WireCodes.Op.ADD_BLOCK, new AddBlockHandler(), WireCodes.Op.GET_BLOCKCHAIN,new GetBlockchainHandler(), WireCodes.Op.SEARCH_USERS, new SearchUsersHandler(), WireCodes.Op.GET_LAST_BLOCK_INFO,new GetLastBlockInfoHandler() diff --git a/src/main/java/server/logic/ws_protocol/binary/handlers/AddBlockHandler.java b/src/main/java/server/logic/ws_protocol/binary/handlers/AddBlockHandler.java index 87b2c20..7988623 100644 --- a/src/main/java/server/logic/ws_protocol/binary/handlers/AddBlockHandler.java +++ b/src/main/java/server/logic/ws_protocol/binary/handlers/AddBlockHandler.java @@ -1,250 +1,250 @@ -package server.logic.ws_protocol.binary.handlers; - -import blockchain.BchBlockEntry; -import blockchain.body.BodyRecord; -import blockchain.BodyRecordParser; -import blockchain.body.HeaderBody; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import server.logic.ws_protocol.WireCodes; -import utils.blockchain.BchInfoEntry; -import utils.blockchain.BchInfoManager; -import utils.crypto.BchCryptoVerifier; -import utils.files.FileStoreUtil; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Arrays; - -/** - * AddBlockHandler — обработчик команды "добавить блок" (ADD_BLOCK) - * --------------------------------------------------------------- - * Принимает бинарное сообщение от клиента и добавляет новый блок в цепочку. - *. - * Формат входного сообщения (msg): - * [0..3] — 4 байта: код операции (WireCodes.ADD_BLOCK) - * [4..11] — 8 байт: blockchainId (уникальный идентификатор цепочки) - * [12..] — байты полного блока .bch: - * ├── 4 байта recordSize = M + 18 - * ├── 4 байта recordNumber - * ├── 8 байт timestamp - * ├── 2 байта recordType - * ├── 2 байта recordVersion - * ├── M байт body (содержимое блока) - * ├── 64 байта signature (Ed25519) - * └── 32 байта hash (SHA-256) - *. - * --------------------------------------------------------------- - * Алгоритм работы: - *. - * 1️⃣ Распаковать BchBlockEntry из msg (т.е. выделить тело блока и подписи). - * 2️⃣ Найти описание цепочки (BchInfoEntry) по blockchainId. - *. - * ─ Если описания нет (цепочка ещё не существует): - * • принимаем только блок типа 0 (HeaderBody) и номера 0; - * • парсим его, создаём новый BchInfoEntry на основе данных заголовка; - * • проверяем подпись и хэш; - * • проверяем корректность тела блока (check); - * • сохраняем блок и создаём новый blockchain-файл; - * • добавляем цепочку в менеджер BchInfoManager. - * (💡 временное решение: создание цепочки допустимо только через HeaderBody) - *. - * ─ Если цепочка уже существует: - * • проверяем, что номер блока равен (lastBlockNumber + 1); - * • проверяем подпись и хэш; - * • проверяем тело блока (check); - * • добавляем блок в файл цепочки; - * • обновляем состояние BchInfoEntry (номер, хэш, размер). - *. - * 3️⃣ Если все проверки пройдены — возвращаем статус OK. - *. - * Таким образом, единственное различие между первым блоком и последующими — - * момент инициализации описания цепочки (BchInfoEntry). - * Всё остальное (валидация, подпись, добавление, обновление) выполняется одинаково. - */ -public class AddBlockHandler implements MessageHandler { - - private static final Logger log = LoggerFactory.getLogger(AddBlockHandler.class); - - @Override - public byte[] handle(byte[] msg) { - try { - // ===================================================================== - // 1️⃣ Проверка минимальной длины пакета - // ===================================================================== - int minFull = BchBlockEntry.RAW_HEADER_SIZE + BchBlockEntry.SIGNATURE_LEN + BchBlockEntry.HASH_LEN; - // (RAW_HEADER_SIZE = 18 байт, подпись = 64, хэш = 32) - if (msg.length < 4 + 8 + minFull) - return code(WireCodes.Status.BAD_REQUEST); - - // ===================================================================== - // 2️⃣ Извлекаем blockchainId (8 байт начиная с позиции 4) - // ===================================================================== - long blockchainId = ByteBuffer.wrap(msg, 4, 8) - .order(ByteOrder.BIG_ENDIAN) - .getLong(); - - // Всё, что дальше, — это бинарное содержимое блока .bch - int offset = 12; // первые 12 байт = код + blockchainId - - // ===================================================================== - // 3️⃣ Парсим блок (RAW + подпись + хэш) - // ===================================================================== - byte[] fullBlock = Arrays.copyOfRange(msg, offset, msg.length); - BchBlockEntry block = new BchBlockEntry(fullBlock); // сам распакует RAW-часть и подписи - - // ===================================================================== - // 4️⃣ Получаем текущее описание цепочки (BchInfoEntry) - // ===================================================================== - BchInfoManager info = BchInfoManager.getInstance(); - BchInfoEntry chain = info.getBchInfo(blockchainId); - - byte[] prevHash32; - int expectedNum; - String userLogin; - byte[] publicKey32; - - // ===================================================================== - // 🧩 СЦЕНАРИЙ 1: цепочка отсутствует — создаём новую - // ===================================================================== - if (chain == null) { - // Допускаем только блок-заголовок (type=0, num=0) - if (block.recordType != BchBlockEntry.TYPE_HEADER || block.recordNumber != 0) { - log.warn("Попытка создать новую цепочку без корректного заголовка (type={}, num={})", - block.recordType, block.recordNumber); - return code(WireCodes.Status.BAD_REQUEST); - } - - // Парсим тело блока → HeaderBody - BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check(); - if (!(body instanceof HeaderBody)) - return code(WireCodes.Status.BAD_REQUEST); - - HeaderBody hb = (HeaderBody) body; - - // Проверяем, что blockchainId совпадает - if (hb.blockchainId != blockchainId) { - log.warn("Несовпадение blockchainId в заголовке (ожидалось {}, получено {})", - blockchainId, hb.blockchainId); - return code(WireCodes.Status.BAD_REQUEST); - } - - // Проверяем подпись и хэш первого блока (предыдущий хэш = 0) - prevHash32 = new byte[32]; - boolean verified = BchCryptoVerifier.verifyAll( - hb.userLogin, - blockchainId, - prevHash32, - block.rawBytes, - block.getSignature64(), - block.getHash32(), - hb.publicKey32 - ); - if (!verified) { - log.warn("❌ Подпись не прошла проверку при создании цепочки blockchainId={}", blockchainId); - return code(WireCodes.Status.UNVERIFIED); - } - - // ✅ Всё хорошо: создаём новую цепочку - info.addBlockchain(blockchainId, hb.userLogin, hb.publicKey32, Integer.MAX_VALUE); - info.updateBlockchainState(blockchainId, block.recordNumber, bytesToHex(block.getHash32()), fullBlock.length); - - FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, fullBlock); - - log.info("✅ Создана новая цепочка blockchainId={}, user={}, blockNum={}", - blockchainId, hb.userLogin, block.recordNumber); - - return code(WireCodes.Status.OK); - } - - // ===================================================================== - // 🧩 СЦЕНАРИЙ 2: цепочка существует — добавляем новый блок - // ===================================================================== - expectedNum = chain.lastBlockNumber + 1; - - // Проверка последовательности (и отправка lastBlockNumber) - if (block.recordNumber < expectedNum) { - log.info("🔁 Блок {} уже существует, последний = {}", block.recordNumber, chain.lastBlockNumber); - ByteBuffer out = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN); - out.putInt(WireCodes.Status.BLOCK_ALREADY_EXISTS); - out.putInt(chain.lastBlockNumber); - return out.array(); - } - if (block.recordNumber > expectedNum) { - log.warn("⚠️ Нарушена последовательность: получен {}, ожидался {}", block.recordNumber, expectedNum); - ByteBuffer out = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN); - out.putInt(WireCodes.Status.OUT_OF_SEQUENCE); - out.putInt(chain.lastBlockNumber); - return out.array(); - } - - userLogin = chain.userLogin; - publicKey32 = chain.getPublicKey32(); - - // Хэш предыдущего блока (или 32 нуля, если это первый) - prevHash32 = (chain.lastBlockHash == null || chain.lastBlockHash.isEmpty()) - ? new byte[32] - : hexToBytes(chain.lastBlockHash); - - // Проверяем подпись и хэш - boolean verified = BchCryptoVerifier.verifyAll( - userLogin, - blockchainId, - prevHash32, - block.rawBytes, - block.getSignature64(), - block.getHash32(), - publicKey32 - ); - if (!verified) { - log.warn("❌ Подпись не прошла проверку: chainId={}, blockNum={}", blockchainId, block.recordNumber); - return code(WireCodes.Status.UNVERIFIED); - } - - // Проверяем тело блока (например, корректный UTF-8 или структура) - BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check(); - - // ✅ Добавляем блок в файл цепочки - FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, fullBlock); - - // Обновляем состояние цепочки (номер, хэш, размер) - int newSize = chain.blockchainSize + fullBlock.length; - info.updateBlockchainState(blockchainId, block.recordNumber, bytesToHex(block.getHash32()), newSize); - - log.info("✅ Блок добавлен: chain={}, num={}, type={}, bytes={}", - blockchainId, block.recordNumber, block.recordType, fullBlock.length); - - return code(WireCodes.Status.OK); - - } catch (Exception e) { - log.error("❌ ADD_BLOCK: внутренняя ошибка при обработке", e); - return code(WireCodes.Status.INTERNAL_ERROR); - } - } - - // ===================================================================== - // Утилиты - // ===================================================================== - - /** Преобразовать статус (int) в 4 байта BigEndian. */ - private static byte[] code(int status) { - return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(status).array(); - } - - /** Конвертация HEX → bytes (для хэшей). */ - private static byte[] hexToBytes(String hex) { - int len = hex.length(); - byte[] out = new byte[len / 2]; - for (int i = 0; i < len; i += 2) - out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16); - return out; - } - - /** Конвертация bytes → HEX (для сохранения в BchInfo). */ - private static String bytesToHex(byte[] b) { - StringBuilder sb = new StringBuilder(b.length * 2); - for (byte x : b) sb.append(String.format("%02x", x)); - return sb.toString(); - } -} - +//package server.logic.ws_protocol.binary.handlers; +// +//import blockchain.BchBlockEntry; +//import blockchain.body.BodyRecord; +//import blockchain.BodyRecordParser; +//import blockchain.body.HeaderBody; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import server.logic.ws_protocol.WireCodes; +//import utils.blockchain.BchInfoEntry; +//import utils.blockchain.BchInfoManager; +//import utils.crypto.BchCryptoVerifier; +//import utils.files.FileStoreUtil; +// +//import java.nio.ByteBuffer; +//import java.nio.ByteOrder; +//import java.util.Arrays; +// +///** +// * AddBlockHandler — обработчик команды "добавить блок" (ADD_BLOCK) +// * --------------------------------------------------------------- +// * Принимает бинарное сообщение от клиента и добавляет новый блок в цепочку. +// *. +// * Формат входного сообщения (msg): +// * [0..3] — 4 байта: код операции (WireCodes.ADD_BLOCK) +// * [4..11] — 8 байт: blockchainId (уникальный идентификатор цепочки) +// * [12..] — байты полного блока .bch: +// * ├── 4 байта recordSize = M + 18 +// * ├── 4 байта recordNumber +// * ├── 8 байт timestamp +// * ├── 2 байта recordType +// * ├── 2 байта recordVersion +// * ├── M байт body (содержимое блока) +// * ├── 64 байта signature (Ed25519) +// * └── 32 байта hash (SHA-256) +// *. +// * --------------------------------------------------------------- +// * Алгоритм работы: +// *. +// * 1️⃣ Распаковать BchBlockEntry из msg (т.е. выделить тело блока и подписи). +// * 2️⃣ Найти описание цепочки (BchInfoEntry) по blockchainId. +// *. +// * ─ Если описания нет (цепочка ещё не существует): +// * • принимаем только блок типа 0 (HeaderBody) и номера 0; +// * • парсим его, создаём новый BchInfoEntry на основе данных заголовка; +// * • проверяем подпись и хэш; +// * • проверяем корректность тела блока (check); +// * • сохраняем блок и создаём новый blockchain-файл; +// * • добавляем цепочку в менеджер BchInfoManager. +// * (💡 временное решение: создание цепочки допустимо только через HeaderBody) +// *. +// * ─ Если цепочка уже существует: +// * • проверяем, что номер блока равен (lastBlockNumber + 1); +// * • проверяем подпись и хэш; +// * • проверяем тело блока (check); +// * • добавляем блок в файл цепочки; +// * • обновляем состояние BchInfoEntry (номер, хэш, размер). +// *. +// * 3️⃣ Если все проверки пройдены — возвращаем статус OK. +// *. +// * Таким образом, единственное различие между первым блоком и последующими — +// * момент инициализации описания цепочки (BchInfoEntry). +// * Всё остальное (валидация, подпись, добавление, обновление) выполняется одинаково. +// */ +//public class AddBlockHandler implements MessageHandler { +// +// private static final Logger log = LoggerFactory.getLogger(AddBlockHandler.class); +// +// @Override +// public byte[] handle(byte[] msg) { +// try { +// // ===================================================================== +// // 1️⃣ Проверка минимальной длины пакета +// // ===================================================================== +// int minFull = BchBlockEntry.RAW_HEADER_SIZE + BchBlockEntry.SIGNATURE_LEN + BchBlockEntry.HASH_LEN; +// // (RAW_HEADER_SIZE = 18 байт, подпись = 64, хэш = 32) +// if (msg.length < 4 + 8 + minFull) +// return code(WireCodes.Status.BAD_REQUEST); +// +// // ===================================================================== +// // 2️⃣ Извлекаем blockchainId (8 байт начиная с позиции 4) +// // ===================================================================== +// long blockchainId = ByteBuffer.wrap(msg, 4, 8) +// .order(ByteOrder.BIG_ENDIAN) +// .getLong(); +// +// // Всё, что дальше, — это бинарное содержимое блока .bch +// int offset = 12; // первые 12 байт = код + blockchainId +// +// // ===================================================================== +// // 3️⃣ Парсим блок (RAW + подпись + хэш) +// // ===================================================================== +// byte[] fullBlock = Arrays.copyOfRange(msg, offset, msg.length); +// BchBlockEntry block = new BchBlockEntry(fullBlock); // сам распакует RAW-часть и подписи +// +// // ===================================================================== +// // 4️⃣ Получаем текущее описание цепочки (BchInfoEntry) +// // ===================================================================== +// BchInfoManager info = BchInfoManager.getInstance(); +// BchInfoEntry chain = info.getBchInfo(blockchainId); +// +// byte[] prevHash32; +// int expectedNum; +// String userLogin; +// byte[] publicKey32; +// +// // ===================================================================== +// // 🧩 СЦЕНАРИЙ 1: цепочка отсутствует — создаём новую +// // ===================================================================== +// if (chain == null) { +// // Допускаем только блок-заголовок (type=0, num=0) +// if (block.recordType != BchBlockEntry.TYPE_HEADER || block.recordNumber != 0) { +// log.warn("Попытка создать новую цепочку без корректного заголовка (type={}, num={})", +// block.recordType, block.recordNumber); +// return code(WireCodes.Status.BAD_REQUEST); +// } +// +// // Парсим тело блока → HeaderBody +// BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check(); +// if (!(body instanceof HeaderBody)) +// return code(WireCodes.Status.BAD_REQUEST); +// +// HeaderBody hb = (HeaderBody) body; +// +// // Проверяем, что blockchainId совпадает +// if (hb.blockchainId != blockchainId) { +// log.warn("Несовпадение blockchainId в заголовке (ожидалось {}, получено {})", +// blockchainId, hb.blockchainId); +// return code(WireCodes.Status.BAD_REQUEST); +// } +// +// // Проверяем подпись и хэш первого блока (предыдущий хэш = 0) +// prevHash32 = new byte[32]; +// boolean verified = BchCryptoVerifier.verifyAll( +// hb.userLogin, +// blockchainId, +// prevHash32, +// block.rawBytes, +// block.getSignature64(), +// block.getHash32(), +// hb.publicKey32 +// ); +// if (!verified) { +// log.warn("❌ Подпись не прошла проверку при создании цепочки blockchainId={}", blockchainId); +// return code(WireCodes.Status.UNVERIFIED); +// } +// +// // ✅ Всё хорошо: создаём новую цепочку +// info.addBlockchain(blockchainId, hb.userLogin, hb.publicKey32, Integer.MAX_VALUE); +// info.updateBlockchainState(blockchainId, block.recordNumber, bytesToHex(block.getHash32()), fullBlock.length); +// +// FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, fullBlock); +// +// log.info("✅ Создана новая цепочка blockchainId={}, user={}, blockNum={}", +// blockchainId, hb.userLogin, block.recordNumber); +// +// return code(WireCodes.Status.OK); +// } +// +// // ===================================================================== +// // 🧩 СЦЕНАРИЙ 2: цепочка существует — добавляем новый блок +// // ===================================================================== +// expectedNum = chain.lastBlockNumber + 1; +// +// // Проверка последовательности (и отправка lastBlockNumber) +// if (block.recordNumber < expectedNum) { +// log.info("🔁 Блок {} уже существует, последний = {}", block.recordNumber, chain.lastBlockNumber); +// ByteBuffer out = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN); +// out.putInt(WireCodes.Status.BLOCK_ALREADY_EXISTS); +// out.putInt(chain.lastBlockNumber); +// return out.array(); +// } +// if (block.recordNumber > expectedNum) { +// log.warn("⚠️ Нарушена последовательность: получен {}, ожидался {}", block.recordNumber, expectedNum); +// ByteBuffer out = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN); +// out.putInt(WireCodes.Status.OUT_OF_SEQUENCE); +// out.putInt(chain.lastBlockNumber); +// return out.array(); +// } +// +// userLogin = chain.userLogin; +// publicKey32 = chain.getPublicKey32(); +// +// // Хэш предыдущего блока (или 32 нуля, если это первый) +// prevHash32 = (chain.lastBlockHash == null || chain.lastBlockHash.isEmpty()) +// ? new byte[32] +// : hexToBytes(chain.lastBlockHash); +// +// // Проверяем подпись и хэш +// boolean verified = BchCryptoVerifier.verifyAll( +// userLogin, +// blockchainId, +// prevHash32, +// block.rawBytes, +// block.getSignature64(), +// block.getHash32(), +// publicKey32 +// ); +// if (!verified) { +// log.warn("❌ Подпись не прошла проверку: chainId={}, blockNum={}", blockchainId, block.recordNumber); +// return code(WireCodes.Status.UNVERIFIED); +// } +// +// // Проверяем тело блока (например, корректный UTF-8 или структура) +// BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check(); +// +// // ✅ Добавляем блок в файл цепочки +// FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, fullBlock); +// +// // Обновляем состояние цепочки (номер, хэш, размер) +// int newSize = chain.blockchainSize + fullBlock.length; +// info.updateBlockchainState(blockchainId, block.recordNumber, bytesToHex(block.getHash32()), newSize); +// +// log.info("✅ Блок добавлен: chain={}, num={}, type={}, bytes={}", +// blockchainId, block.recordNumber, block.recordType, fullBlock.length); +// +// return code(WireCodes.Status.OK); +// +// } catch (Exception e) { +// log.error("❌ ADD_BLOCK: внутренняя ошибка при обработке", e); +// return code(WireCodes.Status.INTERNAL_ERROR); +// } +// } +// +// // ===================================================================== +// // Утилиты +// // ===================================================================== +// +// /** Преобразовать статус (int) в 4 байта BigEndian. */ +// private static byte[] code(int status) { +// return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(status).array(); +// } +// +// /** Конвертация HEX → bytes (для хэшей). */ +// private static byte[] hexToBytes(String hex) { +// int len = hex.length(); +// byte[] out = new byte[len / 2]; +// for (int i = 0; i < len; i += 2) +// out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16); +// return out; +// } +// +// /** Конвертация bytes → HEX (для сохранения в BchInfo). */ +// private static String bytesToHex(byte[] b) { +// StringBuilder sb = new StringBuilder(b.length * 2); +// for (byte x : b) sb.append(String.format("%02x", x)); +// return sb.toString(); +// } +//} +// diff --git a/src/main/java/test/it/WsTestClient.java b/src/main/java/test/it/WsTestClient.java new file mode 100644 index 0000000..5b248c6 --- /dev/null +++ b/src/main/java/test/it/WsTestClient.java @@ -0,0 +1,76 @@ +package test.it; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.*; + +public final class WsTestClient implements AutoCloseable { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final WebSocket ws; + private final Map> pending = new ConcurrentHashMap<>(); + + public WsTestClient(String wsUri) { + HttpClient client = HttpClient.newHttpClient(); + this.ws = client.newWebSocketBuilder() + .connectTimeout(Duration.ofSeconds(5)) + .buildAsync(URI.create(wsUri), new WebSocket.Listener() { + @Override + public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { + String msg = data.toString(); + String requestId = extractRequestId(msg); + if (requestId != null) { + CompletableFuture f = pending.remove(requestId); + if (f != null) f.complete(msg); + } + webSocket.request(1); + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + // Завалим все ожидания, чтобы тест корректно упал + pending.forEach((k, f) -> f.completeExceptionally(error)); + pending.clear(); + } + }).join(); + + this.ws.request(1); + } + + public String request(String requestId, String json, Duration timeout) { + CompletableFuture fut = new CompletableFuture<>(); + pending.put(requestId, fut); + ws.sendText(json, true); + try { + return fut.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (Exception e) { + pending.remove(requestId); + throw new RuntimeException("Timeout/Fail waiting response requestId=" + requestId, e); + } + } + + private static String extractRequestId(String json) { + try { + JsonNode root = MAPPER.readTree(json); + JsonNode id = root.get("requestId"); + return id != null && !id.isNull() ? id.asText() : null; + } catch (Exception ignored) { + return null; + } + } + + @Override + public void close() { + try { + ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").join(); + } catch (Exception ignored) {} + } +} \ No newline at end of file diff --git a/src/test/java/test/it/AddUserIT.java b/src/test/java/test/it/AddUserIT.java new file mode 100644 index 0000000..524ebfe --- /dev/null +++ b/src/test/java/test/it/AddUserIT.java @@ -0,0 +1,34 @@ +package test.it; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +public class AddUserIT { + + @Test + void addUser_shouldReturn200_orAlreadyExists() { + try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { + + String reqId = "it-adduser-1"; + String resp = client.request(reqId, JsonBuilders.addUser(reqId), Duration.ofSeconds(5)); + + int st = JsonParsers.status(resp); + + // ВАЖНО: тут подставь свой реальный код "уже существует", если он не 200. + // Я оставляю пример: 409. + boolean created = (st == 200); + boolean already = (st == 409); + + if (created) { + System.out.println("✅ AddUser: создан/добавлен (status=200)"); + } else if (already) { + System.out.println("✅ AddUser: возможно уже есть в базе (status=409)"); + } else { + fail("❌ AddUser: неожиданный status=" + st + ", resp=" + resp); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/test/it/JsonBuilders.java b/src/test/java/test/it/JsonBuilders.java new file mode 100644 index 0000000..f3cccfa --- /dev/null +++ b/src/test/java/test/it/JsonBuilders.java @@ -0,0 +1,111 @@ +package test.it; + +import utils.crypto.Ed25519Util; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public final class JsonBuilders { + private JsonBuilders(){} + + public static String addUser(String requestId) { + return """ + { + "op": "AddUser", + "requestId": "%s", + "payload": { + "login": "%s", + "blockchainName": "%s", + "loginKey": "%s", + "deviceKey": "%s", + "bchLimit": %d + } + } + """.formatted( + requestId, + TestConfig.TEST_LOGIN, + TestConfig.TEST_BCH_NAME, + TestConfig.LOGIN_PUBKEY_B64, + TestConfig.DEVICE_PUBKEY_B64, + TestConfig.TEST_BCH_LIMIT + ); + } + + public static String authChallenge(String requestId) { + return """ + { + "op": "AuthChallenge", + "requestId": "%s", + "payload": { "login": "%s" } + } + """.formatted(requestId, TestConfig.TEST_LOGIN); + } + + public static String createAuthSession(String requestId, String authNonce, String storagePwd) { + long timeMs = System.currentTimeMillis(); + String sigB64 = signAuthorificated(authNonce, timeMs); + + return """ + { + "op": "CreateAuthSession", + "requestId": "%s", + "payload": { + "storagePwd": "%s", + "timeMs": %d, + "signatureB64": "%s", + "clientInfo": "%s" + } + } + """.formatted(requestId, storagePwd, timeMs, sigB64, TestConfig.TEST_CLIENT_INFO); + } + + public static String listSessions(String requestId, long timeMs, String signatureB64) { + if (signatureB64 == null) signatureB64 = ""; + return """ + { + "op": "ListSessions", + "requestId": "%s", + "payload": { + "timeMs": %d, + "signatureB64": "%s" + } + } + """.formatted(requestId, timeMs, signatureB64); + } + + public static String refreshSession(String requestId, String sessionId, String sessionPwd) { + return """ + { + "op": "RefreshSession", + "requestId": "%s", + "payload": { + "sessionId": "%s", + "sessionPwd": "%s", + "clientInfo": "%s" + } + } + """.formatted(requestId, sessionId, sessionPwd, TestConfig.TEST_CLIENT_INFO); + } + + public static String closeActiveSession(String requestId, String sessionId, long timeMs, String signatureB64) { + if (signatureB64 == null) signatureB64 = ""; + return """ + { + "op": "CloseActiveSession", + "requestId": "%s", + "payload": { + "sessionId": "%s", + "timeMs": %d, + "signatureB64": "%s" + } + } + """.formatted(requestId, sessionId, timeMs, signatureB64); + } + + public static String signAuthorificated(String authNonce, long timeMs) { + String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce; + byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); + byte[] sig = Ed25519Util.sign(preimage, TestConfig.DEVICE_PRIV_KEY); + return Base64.getEncoder().encodeToString(sig); + } +} \ No newline at end of file diff --git a/src/test/java/test/it/JsonParsers.java b/src/test/java/test/it/JsonParsers.java new file mode 100644 index 0000000..2bf2c83 --- /dev/null +++ b/src/test/java/test/it/JsonParsers.java @@ -0,0 +1,82 @@ +package test.it; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.List; + +public final class JsonParsers { + private JsonParsers(){} + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static int status(String json) { + try { + JsonNode root = MAPPER.readTree(json); + return root.has("status") ? root.get("status").asInt() : -1; + } catch (Exception e) { + return -1; + } + } + + public static String authNonce(String json) { + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("authNonce")) return payload.get("authNonce").asText(); + return null; + } catch (Exception e) { + return null; + } + } + + public static String sessionId(String json) { + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("sessionId")) return payload.get("sessionId").asText(); + return null; + } catch (Exception e) { + return null; + } + } + + public static String sessionPwd(String json) { + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("sessionPwd")) return payload.get("sessionPwd").asText(); + return null; + } catch (Exception e) { + return null; + } + } + + public static String storagePwd(String json) { + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("storagePwd")) return payload.get("storagePwd").asText(); + return null; + } catch (Exception e) { + return null; + } + } + + public static List sessionIds(String json) { + List res = new ArrayList<>(); + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload == null) return res; + JsonNode arr = payload.get("sessions"); + if (arr == null || !arr.isArray()) return res; + + for (JsonNode s : arr) { + JsonNode id = s.get("sessionId"); + if (id != null && !id.isNull()) res.add(id.asText()); + } + } catch (Exception ignored) {} + return res; + } +} \ No newline at end of file diff --git a/src/test/java/test/it/SessionsIT.java b/src/test/java/test/it/SessionsIT.java new file mode 100644 index 0000000..0755beb --- /dev/null +++ b/src/test/java/test/it/SessionsIT.java @@ -0,0 +1,166 @@ +package test.it; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class SessionsIT { + + @BeforeAll + static void ensureUserExists() { + try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { + String reqId = "it-adduser-beforeall"; + String resp = client.request(reqId, JsonBuilders.addUser(reqId), Duration.ofSeconds(5)); + int st = JsonParsers.status(resp); + + // 200 или "уже есть" — ок + if (!(st == 200 || st == 409)) { + fail("User precondition failed. status=" + st + ", resp=" + resp); + } + } + } + + @Test + void sessions_flow_shouldCreateListRefreshCloseCorrectly() { + String s1Id, s1Pwd; + String s2Id, s2Pwd; + + // --- create session1 --- + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-1"; + String resp1 = c.request(r1, JsonBuilders.authChallenge(r1), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp1)); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + String r2 = "it-create-1"; + String storagePwd = TestConfig.fakeStoragePwd(); + String resp2 = c.request(r2, JsonBuilders.createAuthSession(r2, nonce, storagePwd), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp2)); + + s1Id = JsonParsers.sessionId(resp2); + s1Pwd = JsonParsers.sessionPwd(resp2); + assertNotNull(s1Id); + assertNotNull(s1Pwd); + } + + // --- create session2 and list inside (AUTH_STATUS_USER) --- + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-2"; + String resp1 = c.request(r1, JsonBuilders.authChallenge(r1), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp1)); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + String r2 = "it-create-2"; + String resp2 = c.request(r2, JsonBuilders.createAuthSession(r2, nonce, TestConfig.fakeStoragePwd()), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp2)); + + s2Id = JsonParsers.sessionId(resp2); + s2Pwd = JsonParsers.sessionPwd(resp2); + assertNotNull(s2Id); + assertNotNull(s2Pwd); + + // list inside session2 (у тебя это AUTH_STATUS_USER без подписи) + String r3 = "it-list-in-session2"; + String resp3 = c.request(r3, JsonBuilders.listSessions(r3, 0L, ""), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp3)); + List ids = JsonParsers.sessionIds(resp3); + + assertTrue(ids.contains(s1Id), "Must contain session1"); + assertTrue(ids.contains(s2Id), "Must contain session2"); + } + + // --- list in AUTH_IN_PROGRESS (подпись по nonce) --- + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-list"; + String resp1 = c.request(r1, JsonBuilders.authChallenge(r1), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp1)); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs); + + String r2 = "it-list-auth-in-progress"; + String resp2 = c.request(r2, JsonBuilders.listSessions(r2, timeMs, sig), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp2)); + + List ids = JsonParsers.sessionIds(resp2); + assertTrue(ids.contains(s1Id)); + assertTrue(ids.contains(s2Id)); + } + + // --- refresh session1 and close session2 (from session1) --- + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + + String r1 = "it-refresh-s1"; + String resp1 = c.request(r1, JsonBuilders.refreshSession(r1, s1Id, s1Pwd), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp1)); + assertNotNull(JsonParsers.storagePwd(resp1)); + + String r2 = "it-close-s2"; + String resp2 = c.request(r2, JsonBuilders.closeActiveSession(r2, s2Id, 0L, ""), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp2)); + } + + // --- verify only session1 remains (AUTH_IN_PROGRESS list) --- + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-list2"; + String resp1 = c.request(r1, JsonBuilders.authChallenge(r1), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp1)); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs); + + String r2 = "it-list-after-close-s2"; + String resp2 = c.request(r2, JsonBuilders.listSessions(r2, timeMs, sig), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp2)); + + List ids = JsonParsers.sessionIds(resp2); + assertTrue(ids.contains(s1Id)); + assertFalse(ids.contains(s2Id)); + } + + // --- close session1 in AUTH_IN_PROGRESS --- + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-close-s1"; + String resp1 = c.request(r1, JsonBuilders.authChallenge(r1), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp1)); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs); + + String r2 = "it-close-s1"; + String resp2 = c.request(r2, JsonBuilders.closeActiveSession(r2, s1Id, timeMs, sig), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp2)); + } + + // --- verify empty list --- + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-list-empty"; + String resp1 = c.request(r1, JsonBuilders.authChallenge(r1), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp1)); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs); + + String r2 = "it-list-empty"; + String resp2 = c.request(r2, JsonBuilders.listSessions(r2, timeMs, sig), Duration.ofSeconds(5)); + assertEquals(200, JsonParsers.status(resp2)); + + List ids = JsonParsers.sessionIds(resp2); + assertTrue(ids.isEmpty(), "Sessions must be empty"); + } + } +} \ No newline at end of file diff --git a/src/test/java/test/it/TestConfig.java b/src/test/java/test/it/TestConfig.java new file mode 100644 index 0000000..0bdfa7f --- /dev/null +++ b/src/test/java/test/it/TestConfig.java @@ -0,0 +1,37 @@ +package test.it; + +import utils.crypto.Ed25519Util; + +import java.util.Base64; + +public final class TestConfig { + private TestConfig(){} + + public static final String WS_URI = "ws://localhost:7070/ws"; + public static final String TEST_LOGIN = "anya24"; + public static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; + public static final int TEST_BCH_LIMIT = 1_000_000; + public static final String TEST_CLIENT_INFO = "JavaTestClient/1.0"; + + public static final byte[] LOGIN_PRIV_KEY; + public static final String LOGIN_PUBKEY_B64; + + public static final byte[] DEVICE_PRIV_KEY; + public static final String DEVICE_PUBKEY_B64; + + static { + LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN); + byte[] loginPub = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY); + LOGIN_PUBKEY_B64 = Ed25519Util.keyToBase64(loginPub); + + DEVICE_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-device-" + TEST_LOGIN); + byte[] devicePub = Ed25519Util.derivePublicKey(DEVICE_PRIV_KEY); + DEVICE_PUBKEY_B64 = Ed25519Util.keyToBase64(devicePub); + } + + public static String fakeStoragePwd() { + byte[] data = new byte[32]; + for (int i = 0; i < data.length; i++) data[i] = (byte) (i + 1); + return Base64.getEncoder().encodeToString(data); + } +} \ No newline at end of file