23 12 25
Прошли тесты на создание сессии - посути всё работает (но добавление блоков пока не работает)
This commit is contained in:
parent
03b6ff3c32
commit
ae63a653c8
@ -1,6 +1,5 @@
|
|||||||
package blockchain;
|
package blockchain;
|
||||||
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import utils.blockchain.BchInfoEntry;
|
import utils.blockchain.BchInfoEntry;
|
||||||
@ -24,11 +23,6 @@ public final class BchBlockValidator {
|
|||||||
/**
|
/**
|
||||||
* Проверяет, что блок может быть корректно добавлен к цепочке.
|
* Проверяет, что блок может быть корректно добавлен к цепочке.
|
||||||
*
|
*
|
||||||
* Не используется при получении запроса на добавление блока по сети (тк там возвращаются более протоколо осмысленные коды
|
|
||||||
* если блок не подходит по номеру.
|
|
||||||
*
|
|
||||||
* А этот класс может быть использован в будущем для внутренних, повторных проверок существующих цепочек блоков.
|
|
||||||
*
|
|
||||||
* @param block блок (распарсенный из байт)
|
* @param block блок (распарсенный из байт)
|
||||||
* @param chain информация о цепочке (BchInfoEntry)
|
* @param chain информация о цепочке (BchInfoEntry)
|
||||||
* @param chainId идентификатор цепочки
|
* @param chainId идентификатор цепочки
|
||||||
@ -51,6 +45,19 @@ public final class BchBlockValidator {
|
|||||||
return false;
|
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️⃣ Проверка публичного ключа
|
// 2️⃣ Проверка публичного ключа
|
||||||
byte[] publicKey = chain.getPublicKey32();
|
byte[] publicKey = chain.getPublicKey32();
|
||||||
if (publicKey == null || publicKey.length != 32) {
|
if (publicKey == null || publicKey.length != 32) {
|
||||||
@ -100,4 +107,4 @@ public final class BchBlockValidator {
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,322 +1,322 @@
|
|||||||
package Test;
|
//package Test;
|
||||||
|
//
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
//import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
//import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import utils.crypto.Ed25519Util;
|
//import utils.crypto.Ed25519Util;
|
||||||
import blockchain.body.HeaderBody;
|
//import blockchain.body.HeaderBody;
|
||||||
import blockchain.body.TextBody;
|
//import blockchain.body.TextBody;
|
||||||
import blockchain_new.BchCryptoVerifier_new;
|
//import blockchain_new.BchCryptoVerifier_new;
|
||||||
import blockchain_new.BchBlockEntry_new;
|
//import blockchain_new.BchBlockEntry_new;
|
||||||
|
//
|
||||||
import java.net.URI;
|
//import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
//import java.net.http.HttpClient;
|
||||||
import java.net.http.WebSocket;
|
//import java.net.http.WebSocket;
|
||||||
import java.nio.ByteBuffer;
|
//import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
//import java.nio.ByteOrder;
|
||||||
import java.util.Base64;
|
//import java.util.Base64;
|
||||||
import java.util.concurrent.CompletableFuture;
|
//import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionStage;
|
//import java.util.concurrent.CompletionStage;
|
||||||
import java.util.concurrent.CountDownLatch;
|
//import java.util.concurrent.CountDownLatch;
|
||||||
|
//
|
||||||
public class Test_AddBlock_new_NoAuth {
|
//public class Test_AddBlock_new_NoAuth {
|
||||||
|
//
|
||||||
private static final String WS_URI = "ws://localhost:7070/ws";
|
// private static final String WS_URI = "ws://localhost:7070/ws";
|
||||||
private static final ObjectMapper JSON = new ObjectMapper();
|
// private static final ObjectMapper JSON = new ObjectMapper();
|
||||||
|
//
|
||||||
private static final String TEST_LOGIN = "anya24";
|
// private static final String TEST_LOGIN = "anya24";
|
||||||
// По твоему правилу: blockchainName = login + 4 цифры
|
// // По твоему правилу: blockchainName = login + 4 цифры
|
||||||
private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
|
// private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
|
||||||
|
//
|
||||||
private static final byte[] LOGIN_PRIV_KEY;
|
// private static final byte[] LOGIN_PRIV_KEY;
|
||||||
private static final byte[] LOGIN_PUB_KEY;
|
// private static final byte[] LOGIN_PUB_KEY;
|
||||||
|
//
|
||||||
static {
|
// static {
|
||||||
LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
|
// LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
|
||||||
LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
|
// LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static final byte[] ZERO32 = new byte[32];
|
// private static final byte[] ZERO32 = new byte[32];
|
||||||
private static final String ZERO64 = "0".repeat(64);
|
// private static final String ZERO64 = "0".repeat(64);
|
||||||
|
//
|
||||||
public static void main(String[] args) throws Exception {
|
// public static void main(String[] args) throws Exception {
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
// CountDownLatch latch = new CountDownLatch(1);
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
// HttpClient client = HttpClient.newHttpClient();
|
||||||
|
//
|
||||||
client.newWebSocketBuilder()
|
// client.newWebSocketBuilder()
|
||||||
.buildAsync(URI.create(WS_URI), new WebSocket.Listener() {
|
// .buildAsync(URI.create(WS_URI), new WebSocket.Listener() {
|
||||||
|
//
|
||||||
private int step = 0;
|
// private int step = 0;
|
||||||
|
//
|
||||||
// Эти значения обновим ПО ОТВЕТУ сервера на header
|
// // Эти значения обновим ПО ОТВЕТУ сервера на header
|
||||||
private String lastGlobalHashHex = ZERO64;
|
// private String lastGlobalHashHex = ZERO64;
|
||||||
private String lastLineHashHex = ZERO64;
|
// private String lastLineHashHex = ZERO64;
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public void onOpen(WebSocket ws) {
|
// public void onOpen(WebSocket ws) {
|
||||||
System.out.println("✅ WS connected: " + WS_URI);
|
// System.out.println("✅ WS connected: " + WS_URI);
|
||||||
ws.request(1);
|
// ws.request(1);
|
||||||
|
//
|
||||||
// 1) HEADER (global=0, line=0, lineNumber=0)
|
// // 1) HEADER (global=0, line=0, lineNumber=0)
|
||||||
byte[] headerFull = buildHeaderBlockFullBytes(
|
// byte[] headerFull = buildHeaderBlockFullBytes(
|
||||||
/*global*/0,
|
// /*global*/0,
|
||||||
/*lineIndex*/(short)0,
|
// /*lineIndex*/(short)0,
|
||||||
/*lineBlock*/0,
|
// /*lineBlock*/0,
|
||||||
/*prevGlobal*/ZERO32,
|
// /*prevGlobal*/ZERO32,
|
||||||
/*prevLine*/ZERO32
|
// /*prevLine*/ZERO32
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
String json = buildAddBlockJson(
|
// String json = buildAddBlockJson(
|
||||||
"test-add-header",
|
// "test-add-header",
|
||||||
TEST_BCH_NAME,
|
// TEST_BCH_NAME,
|
||||||
0,
|
// 0,
|
||||||
ZERO64, // prevGlobalHash для первого блока — нули
|
// ZERO64, // prevGlobalHash для первого блока — нули
|
||||||
base64(headerFull)
|
// base64(headerFull)
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
|
// System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
|
||||||
ws.sendText(json, true);
|
// ws.sendText(json, true);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
|
// public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
|
||||||
String msg = data.toString();
|
// String msg = data.toString();
|
||||||
System.out.println("\n📥 RECV:\n" + msg);
|
// System.out.println("\n📥 RECV:\n" + msg);
|
||||||
System.out.println("-----------------------------------------------------");
|
// System.out.println("-----------------------------------------------------");
|
||||||
|
//
|
||||||
try {
|
// try {
|
||||||
int status = extractStatus(msg);
|
// int status = extractStatus(msg);
|
||||||
|
//
|
||||||
if (step == 0) {
|
// if (step == 0) {
|
||||||
if (status != 200) {
|
// if (status != 200) {
|
||||||
System.out.println("❌ HEADER rejected, status=" + status);
|
// System.out.println("❌ HEADER rejected, status=" + status);
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
|
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Берём ИМЕННО ТОТ хэш, который сервер сохранил в state
|
// // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state
|
||||||
String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash");
|
// String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash");
|
||||||
String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash");
|
// String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash");
|
||||||
|
//
|
||||||
if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) {
|
// if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) {
|
||||||
System.out.println("❌ No serverLastGlobalHash in response");
|
// System.out.println("❌ No serverLastGlobalHash in response");
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response");
|
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response");
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
if (serverLastLineHash == null || serverLastLineHash.isBlank()) {
|
// if (serverLastLineHash == null || serverLastLineHash.isBlank()) {
|
||||||
// fallback: пусть будет как global (если сервер так хранит)
|
// // fallback: пусть будет как global (если сервер так хранит)
|
||||||
serverLastLineHash = serverLastGlobalHash;
|
// serverLastLineHash = serverLastGlobalHash;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
lastGlobalHashHex = serverLastGlobalHash;
|
// lastGlobalHashHex = serverLastGlobalHash;
|
||||||
lastLineHashHex = serverLastLineHash;
|
// lastLineHashHex = serverLastLineHash;
|
||||||
|
//
|
||||||
byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex);
|
// byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex);
|
||||||
byte[] prevLine32 = hexToBytes32(lastLineHashHex);
|
// byte[] prevLine32 = hexToBytes32(lastLineHashHex);
|
||||||
|
//
|
||||||
// 2) TEXT (global=1, line=0, lineNumber=1)
|
// // 2) TEXT (global=1, line=0, lineNumber=1)
|
||||||
byte[] textFull = buildTextBlockFullBytes(
|
// byte[] textFull = buildTextBlockFullBytes(
|
||||||
/*global*/1,
|
// /*global*/1,
|
||||||
/*lineIndex*/(short)0,
|
// /*lineIndex*/(short)0,
|
||||||
/*lineBlock*/1,
|
// /*lineBlock*/1,
|
||||||
prevGlobal32,
|
// prevGlobal32,
|
||||||
prevLine32,
|
// prevLine32,
|
||||||
"Hello from test client"
|
// "Hello from test client"
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
String json2 = buildAddBlockJson(
|
// String json2 = buildAddBlockJson(
|
||||||
"test-add-text",
|
// "test-add-text",
|
||||||
TEST_BCH_NAME,
|
// TEST_BCH_NAME,
|
||||||
1,
|
// 1,
|
||||||
lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера
|
// lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера
|
||||||
base64(textFull)
|
// base64(textFull)
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
|
// System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
|
||||||
step = 1;
|
// step = 1;
|
||||||
ws.sendText(json2, true);
|
// ws.sendText(json2, true);
|
||||||
|
//
|
||||||
} else if (step == 1) {
|
// } else if (step == 1) {
|
||||||
if (status != 200) {
|
// if (status != 200) {
|
||||||
System.out.println("❌ TEXT rejected, status=" + status);
|
// System.out.println("❌ TEXT rejected, status=" + status);
|
||||||
} else {
|
// } else {
|
||||||
System.out.println("✅ Done. Closing.");
|
// System.out.println("✅ Done. Closing.");
|
||||||
}
|
// }
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
|
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
e.printStackTrace(System.out);
|
// e.printStackTrace(System.out);
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
|
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
ws.request(1);
|
// ws.request(1);
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public void onError(WebSocket ws, Throwable error) {
|
// public void onError(WebSocket ws, Throwable error) {
|
||||||
System.out.println("❌ WS error: " + error.getMessage());
|
// System.out.println("❌ WS error: " + error.getMessage());
|
||||||
error.printStackTrace(System.out);
|
// error.printStackTrace(System.out);
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
|
// public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
|
||||||
System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
|
// System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
}).join();
|
// }).join();
|
||||||
|
//
|
||||||
latch.await();
|
// latch.await();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
// BUILD BLOCKS
|
// // BUILD BLOCKS
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
|
//
|
||||||
private static byte[] buildHeaderBlockFullBytes(int globalNumber,
|
// private static byte[] buildHeaderBlockFullBytes(int globalNumber,
|
||||||
short lineIndex,
|
// short lineIndex,
|
||||||
int lineBlockNumber,
|
// int lineBlockNumber,
|
||||||
byte[] prevGlobalHash32,
|
// byte[] prevGlobalHash32,
|
||||||
byte[] prevLineHash32) {
|
// byte[] prevLineHash32) {
|
||||||
|
//
|
||||||
HeaderBody body = new HeaderBody(
|
// HeaderBody body = new HeaderBody(
|
||||||
TEST_BCH_NAME, // было TEST_BCH_ID (long), теперь имя блокчейна (String)
|
// TEST_BCH_NAME, // было TEST_BCH_ID (long), теперь имя блокчейна (String)
|
||||||
TEST_LOGIN,
|
// TEST_LOGIN,
|
||||||
0, 0,
|
// 0, 0,
|
||||||
(short) 1,
|
// (short) 1,
|
||||||
0L,
|
// 0L,
|
||||||
LOGIN_PUB_KEY
|
// LOGIN_PUB_KEY
|
||||||
);
|
// );
|
||||||
byte[] bodyBytes = body.toBytes();
|
// byte[] bodyBytes = body.toBytes();
|
||||||
|
//
|
||||||
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static byte[] buildTextBlockFullBytes(int globalNumber,
|
// private static byte[] buildTextBlockFullBytes(int globalNumber,
|
||||||
short lineIndex,
|
// short lineIndex,
|
||||||
int lineBlockNumber,
|
// int lineBlockNumber,
|
||||||
byte[] prevGlobalHash32,
|
// byte[] prevGlobalHash32,
|
||||||
byte[] prevLineHash32,
|
// byte[] prevLineHash32,
|
||||||
String text) {
|
// String text) {
|
||||||
|
//
|
||||||
TextBody body = new TextBody(text);
|
// TextBody body = new TextBody(text);
|
||||||
byte[] bodyBytes = body.toBytes();
|
// byte[] bodyBytes = body.toBytes();
|
||||||
|
//
|
||||||
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static byte[] buildSignedBlockFullBytes(int globalNumber,
|
// private static byte[] buildSignedBlockFullBytes(int globalNumber,
|
||||||
short lineIndex,
|
// short lineIndex,
|
||||||
int lineBlockNumber,
|
// int lineBlockNumber,
|
||||||
byte[] bodyBytes,
|
// byte[] bodyBytes,
|
||||||
byte[] prevGlobalHash32,
|
// byte[] prevGlobalHash32,
|
||||||
byte[] prevLineHash32) {
|
// byte[] prevLineHash32) {
|
||||||
|
//
|
||||||
long ts = System.currentTimeMillis() / 1000L;
|
// long ts = System.currentTimeMillis() / 1000L;
|
||||||
|
//
|
||||||
int recordSize =
|
// int recordSize =
|
||||||
BchBlockEntry_new.RAW_HEADER_SIZE +
|
// BchBlockEntry_new.RAW_HEADER_SIZE +
|
||||||
bodyBytes.length +
|
// bodyBytes.length +
|
||||||
BchBlockEntry_new.SIGNATURE_LEN +
|
// BchBlockEntry_new.SIGNATURE_LEN +
|
||||||
BchBlockEntry_new.HASH_LEN;
|
// BchBlockEntry_new.HASH_LEN;
|
||||||
|
//
|
||||||
byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry_new.RAW_HEADER_SIZE + bodyBytes.length)
|
// byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry_new.RAW_HEADER_SIZE + bodyBytes.length)
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
// .order(ByteOrder.BIG_ENDIAN)
|
||||||
.putInt(recordSize)
|
// .putInt(recordSize)
|
||||||
.putInt(globalNumber)
|
// .putInt(globalNumber)
|
||||||
.putLong(ts)
|
// .putLong(ts)
|
||||||
.putShort(lineIndex)
|
// .putShort(lineIndex)
|
||||||
.putInt(lineBlockNumber)
|
// .putInt(lineBlockNumber)
|
||||||
.put(bodyBytes)
|
// .put(bodyBytes)
|
||||||
.array();
|
// .array();
|
||||||
|
//
|
||||||
byte[] preimage = BchCryptoVerifier_new.buildPreimage(
|
// byte[] preimage = BchCryptoVerifier_new.buildPreimage(
|
||||||
TEST_LOGIN,
|
// TEST_LOGIN,
|
||||||
prevGlobalHash32,
|
// prevGlobalHash32,
|
||||||
prevLineHash32,
|
// prevLineHash32,
|
||||||
rawBytes
|
// rawBytes
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
byte[] hash32 = BchCryptoVerifier_new.sha256(preimage);
|
// byte[] hash32 = BchCryptoVerifier_new.sha256(preimage);
|
||||||
|
//
|
||||||
// если у тебя подпись должна быть по preimage — меняй тут
|
// // если у тебя подпись должна быть по preimage — меняй тут
|
||||||
byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY);
|
// byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY);
|
||||||
|
//
|
||||||
return new BchBlockEntry_new(
|
// return new BchBlockEntry_new(
|
||||||
globalNumber,
|
// globalNumber,
|
||||||
ts,
|
// ts,
|
||||||
lineIndex,
|
// lineIndex,
|
||||||
lineBlockNumber,
|
// lineBlockNumber,
|
||||||
bodyBytes,
|
// bodyBytes,
|
||||||
signature64,
|
// signature64,
|
||||||
hash32
|
// hash32
|
||||||
).toBytes();
|
// ).toBytes();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
// JSON BUILD
|
// // JSON BUILD
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
|
//
|
||||||
private static String buildAddBlockJson(String requestId,
|
// private static String buildAddBlockJson(String requestId,
|
||||||
String blockchainName,
|
// String blockchainName,
|
||||||
int globalNumber,
|
// int globalNumber,
|
||||||
String prevGlobalHashHex,
|
// String prevGlobalHashHex,
|
||||||
String blockBytesB64) {
|
// String blockBytesB64) {
|
||||||
return """
|
// return """
|
||||||
{
|
// {
|
||||||
"op": "AddBlock",
|
// "op": "AddBlock",
|
||||||
"requestId": "%s",
|
// "requestId": "%s",
|
||||||
"payload": {
|
// "payload": {
|
||||||
"login": "%s",
|
// "login": "%s",
|
||||||
"blockchainName": "%s",
|
// "blockchainName": "%s",
|
||||||
"globalNumber": %d,
|
// "globalNumber": %d,
|
||||||
"prevGlobalHash": "%s",
|
// "prevGlobalHash": "%s",
|
||||||
"blockBytesB64": "%s"
|
// "blockBytesB64": "%s"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
""".formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
|
// """.formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
// HELPERS
|
// // HELPERS
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
|
//
|
||||||
private static int extractStatus(String json) {
|
// private static int extractStatus(String json) {
|
||||||
try {
|
// try {
|
||||||
JsonNode root = JSON.readTree(json);
|
// JsonNode root = JSON.readTree(json);
|
||||||
if (root.has("status")) return root.get("status").asInt();
|
// if (root.has("status")) return root.get("status").asInt();
|
||||||
} catch (Exception ignore) {}
|
// } catch (Exception ignore) {}
|
||||||
return -1;
|
// return -1;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static String extractPayloadString(String json, String field) {
|
// private static String extractPayloadString(String json, String field) {
|
||||||
try {
|
// try {
|
||||||
JsonNode root = JSON.readTree(json);
|
// JsonNode root = JSON.readTree(json);
|
||||||
JsonNode payload = root.get("payload");
|
// JsonNode payload = root.get("payload");
|
||||||
if (payload != null && payload.has(field)) {
|
// if (payload != null && payload.has(field)) {
|
||||||
return payload.get(field).asText();
|
// return payload.get(field).asText();
|
||||||
}
|
// }
|
||||||
} catch (Exception ignore) {}
|
// } catch (Exception ignore) {}
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static String base64(byte[] bytes) {
|
// private static String base64(byte[] bytes) {
|
||||||
return Base64.getEncoder().encodeToString(bytes);
|
// return Base64.getEncoder().encodeToString(bytes);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static byte[] hexToBytes32(String hex) {
|
// private static byte[] hexToBytes32(String hex) {
|
||||||
if (hex == null) throw new IllegalArgumentException("hex is null");
|
// if (hex == null) throw new IllegalArgumentException("hex is null");
|
||||||
String s = hex.trim();
|
// String s = hex.trim();
|
||||||
if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
|
// if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
|
||||||
byte[] out = new byte[32];
|
// byte[] out = new byte[32];
|
||||||
for (int i = 0; i < 32; i++) {
|
// for (int i = 0; i < 32; i++) {
|
||||||
int hi = Character.digit(s.charAt(i * 2), 16);
|
// int hi = Character.digit(s.charAt(i * 2), 16);
|
||||||
int lo = Character.digit(s.charAt(i * 2 + 1), 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));
|
// if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
|
||||||
out[i] = (byte) ((hi << 4) | lo);
|
// out[i] = (byte) ((hi << 4) | lo);
|
||||||
}
|
// }
|
||||||
return out;
|
// return out;
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -760,7 +760,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
"requestId": "test-add-1",
|
"requestId": "test-add-1",
|
||||||
"payload": {
|
"payload": {
|
||||||
"login": "%s",
|
"login": "%s",
|
||||||
"bchName": "%s",
|
"blockchainName": "%s",
|
||||||
"loginKey": "%s",
|
"loginKey": "%s",
|
||||||
"deviceKey": "%s",
|
"deviceKey": "%s",
|
||||||
"bchLimit": %d
|
"bchLimit": %d
|
||||||
|
|||||||
@ -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<BlockParsed> 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<BlockParsed> parseAllBlocks(byte[] file) {
|
|
||||||
List<BlockParsed> 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<byte[]> 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";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ public final class InboundMessageProcessor {
|
|||||||
|
|
||||||
private static final Map<Integer, MessageHandler> HANDLERS = Map.of(
|
private static final Map<Integer, MessageHandler> HANDLERS = Map.of(
|
||||||
WireCodes.Op.PING, new PingHandler(),
|
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.GET_BLOCKCHAIN,new GetBlockchainHandler(),
|
||||||
WireCodes.Op.SEARCH_USERS, new SearchUsersHandler(),
|
WireCodes.Op.SEARCH_USERS, new SearchUsersHandler(),
|
||||||
WireCodes.Op.GET_LAST_BLOCK_INFO,new GetLastBlockInfoHandler()
|
WireCodes.Op.GET_LAST_BLOCK_INFO,new GetLastBlockInfoHandler()
|
||||||
|
|||||||
@ -1,250 +1,250 @@
|
|||||||
package server.logic.ws_protocol.binary.handlers;
|
//package server.logic.ws_protocol.binary.handlers;
|
||||||
|
//
|
||||||
import blockchain.BchBlockEntry;
|
//import blockchain.BchBlockEntry;
|
||||||
import blockchain.body.BodyRecord;
|
//import blockchain.body.BodyRecord;
|
||||||
import blockchain.BodyRecordParser;
|
//import blockchain.BodyRecordParser;
|
||||||
import blockchain.body.HeaderBody;
|
//import blockchain.body.HeaderBody;
|
||||||
import org.slf4j.Logger;
|
//import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
//import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
//import server.logic.ws_protocol.WireCodes;
|
||||||
import utils.blockchain.BchInfoEntry;
|
//import utils.blockchain.BchInfoEntry;
|
||||||
import utils.blockchain.BchInfoManager;
|
//import utils.blockchain.BchInfoManager;
|
||||||
import utils.crypto.BchCryptoVerifier;
|
//import utils.crypto.BchCryptoVerifier;
|
||||||
import utils.files.FileStoreUtil;
|
//import utils.files.FileStoreUtil;
|
||||||
|
//
|
||||||
import java.nio.ByteBuffer;
|
//import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
//import java.nio.ByteOrder;
|
||||||
import java.util.Arrays;
|
//import java.util.Arrays;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* AddBlockHandler — обработчик команды "добавить блок" (ADD_BLOCK)
|
// * AddBlockHandler — обработчик команды "добавить блок" (ADD_BLOCK)
|
||||||
* ---------------------------------------------------------------
|
// * ---------------------------------------------------------------
|
||||||
* Принимает бинарное сообщение от клиента и добавляет новый блок в цепочку.
|
// * Принимает бинарное сообщение от клиента и добавляет новый блок в цепочку.
|
||||||
*.
|
// *.
|
||||||
* Формат входного сообщения (msg):
|
// * Формат входного сообщения (msg):
|
||||||
* [0..3] — 4 байта: код операции (WireCodes.ADD_BLOCK)
|
// * [0..3] — 4 байта: код операции (WireCodes.ADD_BLOCK)
|
||||||
* [4..11] — 8 байт: blockchainId (уникальный идентификатор цепочки)
|
// * [4..11] — 8 байт: blockchainId (уникальный идентификатор цепочки)
|
||||||
* [12..] — байты полного блока .bch:
|
// * [12..] — байты полного блока .bch:
|
||||||
* ├── 4 байта recordSize = M + 18
|
// * ├── 4 байта recordSize = M + 18
|
||||||
* ├── 4 байта recordNumber
|
// * ├── 4 байта recordNumber
|
||||||
* ├── 8 байт timestamp
|
// * ├── 8 байт timestamp
|
||||||
* ├── 2 байта recordType
|
// * ├── 2 байта recordType
|
||||||
* ├── 2 байта recordVersion
|
// * ├── 2 байта recordVersion
|
||||||
* ├── M байт body (содержимое блока)
|
// * ├── M байт body (содержимое блока)
|
||||||
* ├── 64 байта signature (Ed25519)
|
// * ├── 64 байта signature (Ed25519)
|
||||||
* └── 32 байта hash (SHA-256)
|
// * └── 32 байта hash (SHA-256)
|
||||||
*.
|
// *.
|
||||||
* ---------------------------------------------------------------
|
// * ---------------------------------------------------------------
|
||||||
* Алгоритм работы:
|
// * Алгоритм работы:
|
||||||
*.
|
// *.
|
||||||
* 1️⃣ Распаковать BchBlockEntry из msg (т.е. выделить тело блока и подписи).
|
// * 1️⃣ Распаковать BchBlockEntry из msg (т.е. выделить тело блока и подписи).
|
||||||
* 2️⃣ Найти описание цепочки (BchInfoEntry) по blockchainId.
|
// * 2️⃣ Найти описание цепочки (BchInfoEntry) по blockchainId.
|
||||||
*.
|
// *.
|
||||||
* ─ Если описания нет (цепочка ещё не существует):
|
// * ─ Если описания нет (цепочка ещё не существует):
|
||||||
* • принимаем только блок типа 0 (HeaderBody) и номера 0;
|
// * • принимаем только блок типа 0 (HeaderBody) и номера 0;
|
||||||
* • парсим его, создаём новый BchInfoEntry на основе данных заголовка;
|
// * • парсим его, создаём новый BchInfoEntry на основе данных заголовка;
|
||||||
* • проверяем подпись и хэш;
|
// * • проверяем подпись и хэш;
|
||||||
* • проверяем корректность тела блока (check);
|
// * • проверяем корректность тела блока (check);
|
||||||
* • сохраняем блок и создаём новый blockchain-файл;
|
// * • сохраняем блок и создаём новый blockchain-файл;
|
||||||
* • добавляем цепочку в менеджер BchInfoManager.
|
// * • добавляем цепочку в менеджер BchInfoManager.
|
||||||
* (💡 временное решение: создание цепочки допустимо только через HeaderBody)
|
// * (💡 временное решение: создание цепочки допустимо только через HeaderBody)
|
||||||
*.
|
// *.
|
||||||
* ─ Если цепочка уже существует:
|
// * ─ Если цепочка уже существует:
|
||||||
* • проверяем, что номер блока равен (lastBlockNumber + 1);
|
// * • проверяем, что номер блока равен (lastBlockNumber + 1);
|
||||||
* • проверяем подпись и хэш;
|
// * • проверяем подпись и хэш;
|
||||||
* • проверяем тело блока (check);
|
// * • проверяем тело блока (check);
|
||||||
* • добавляем блок в файл цепочки;
|
// * • добавляем блок в файл цепочки;
|
||||||
* • обновляем состояние BchInfoEntry (номер, хэш, размер).
|
// * • обновляем состояние BchInfoEntry (номер, хэш, размер).
|
||||||
*.
|
// *.
|
||||||
* 3️⃣ Если все проверки пройдены — возвращаем статус OK.
|
// * 3️⃣ Если все проверки пройдены — возвращаем статус OK.
|
||||||
*.
|
// *.
|
||||||
* Таким образом, единственное различие между первым блоком и последующими —
|
// * Таким образом, единственное различие между первым блоком и последующими —
|
||||||
* момент инициализации описания цепочки (BchInfoEntry).
|
// * момент инициализации описания цепочки (BchInfoEntry).
|
||||||
* Всё остальное (валидация, подпись, добавление, обновление) выполняется одинаково.
|
// * Всё остальное (валидация, подпись, добавление, обновление) выполняется одинаково.
|
||||||
*/
|
// */
|
||||||
public class AddBlockHandler implements MessageHandler {
|
//public class AddBlockHandler implements MessageHandler {
|
||||||
|
//
|
||||||
private static final Logger log = LoggerFactory.getLogger(AddBlockHandler.class);
|
// private static final Logger log = LoggerFactory.getLogger(AddBlockHandler.class);
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public byte[] handle(byte[] msg) {
|
// public byte[] handle(byte[] msg) {
|
||||||
try {
|
// try {
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
// 1️⃣ Проверка минимальной длины пакета
|
// // 1️⃣ Проверка минимальной длины пакета
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
int minFull = BchBlockEntry.RAW_HEADER_SIZE + BchBlockEntry.SIGNATURE_LEN + BchBlockEntry.HASH_LEN;
|
// int minFull = BchBlockEntry.RAW_HEADER_SIZE + BchBlockEntry.SIGNATURE_LEN + BchBlockEntry.HASH_LEN;
|
||||||
// (RAW_HEADER_SIZE = 18 байт, подпись = 64, хэш = 32)
|
// // (RAW_HEADER_SIZE = 18 байт, подпись = 64, хэш = 32)
|
||||||
if (msg.length < 4 + 8 + minFull)
|
// if (msg.length < 4 + 8 + minFull)
|
||||||
return code(WireCodes.Status.BAD_REQUEST);
|
// return code(WireCodes.Status.BAD_REQUEST);
|
||||||
|
//
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
// 2️⃣ Извлекаем blockchainId (8 байт начиная с позиции 4)
|
// // 2️⃣ Извлекаем blockchainId (8 байт начиная с позиции 4)
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
long blockchainId = ByteBuffer.wrap(msg, 4, 8)
|
// long blockchainId = ByteBuffer.wrap(msg, 4, 8)
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
// .order(ByteOrder.BIG_ENDIAN)
|
||||||
.getLong();
|
// .getLong();
|
||||||
|
//
|
||||||
// Всё, что дальше, — это бинарное содержимое блока .bch
|
// // Всё, что дальше, — это бинарное содержимое блока .bch
|
||||||
int offset = 12; // первые 12 байт = код + blockchainId
|
// int offset = 12; // первые 12 байт = код + blockchainId
|
||||||
|
//
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
// 3️⃣ Парсим блок (RAW + подпись + хэш)
|
// // 3️⃣ Парсим блок (RAW + подпись + хэш)
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
byte[] fullBlock = Arrays.copyOfRange(msg, offset, msg.length);
|
// byte[] fullBlock = Arrays.copyOfRange(msg, offset, msg.length);
|
||||||
BchBlockEntry block = new BchBlockEntry(fullBlock); // сам распакует RAW-часть и подписи
|
// BchBlockEntry block = new BchBlockEntry(fullBlock); // сам распакует RAW-часть и подписи
|
||||||
|
//
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
// 4️⃣ Получаем текущее описание цепочки (BchInfoEntry)
|
// // 4️⃣ Получаем текущее описание цепочки (BchInfoEntry)
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
BchInfoManager info = BchInfoManager.getInstance();
|
// BchInfoManager info = BchInfoManager.getInstance();
|
||||||
BchInfoEntry chain = info.getBchInfo(blockchainId);
|
// BchInfoEntry chain = info.getBchInfo(blockchainId);
|
||||||
|
//
|
||||||
byte[] prevHash32;
|
// byte[] prevHash32;
|
||||||
int expectedNum;
|
// int expectedNum;
|
||||||
String userLogin;
|
// String userLogin;
|
||||||
byte[] publicKey32;
|
// byte[] publicKey32;
|
||||||
|
//
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
// 🧩 СЦЕНАРИЙ 1: цепочка отсутствует — создаём новую
|
// // 🧩 СЦЕНАРИЙ 1: цепочка отсутствует — создаём новую
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
if (chain == null) {
|
// if (chain == null) {
|
||||||
// Допускаем только блок-заголовок (type=0, num=0)
|
// // Допускаем только блок-заголовок (type=0, num=0)
|
||||||
if (block.recordType != BchBlockEntry.TYPE_HEADER || block.recordNumber != 0) {
|
// if (block.recordType != BchBlockEntry.TYPE_HEADER || block.recordNumber != 0) {
|
||||||
log.warn("Попытка создать новую цепочку без корректного заголовка (type={}, num={})",
|
// log.warn("Попытка создать новую цепочку без корректного заголовка (type={}, num={})",
|
||||||
block.recordType, block.recordNumber);
|
// block.recordType, block.recordNumber);
|
||||||
return code(WireCodes.Status.BAD_REQUEST);
|
// return code(WireCodes.Status.BAD_REQUEST);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Парсим тело блока → HeaderBody
|
// // Парсим тело блока → HeaderBody
|
||||||
BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check();
|
// BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check();
|
||||||
if (!(body instanceof HeaderBody))
|
// if (!(body instanceof HeaderBody))
|
||||||
return code(WireCodes.Status.BAD_REQUEST);
|
// return code(WireCodes.Status.BAD_REQUEST);
|
||||||
|
//
|
||||||
HeaderBody hb = (HeaderBody) body;
|
// HeaderBody hb = (HeaderBody) body;
|
||||||
|
//
|
||||||
// Проверяем, что blockchainId совпадает
|
// // Проверяем, что blockchainId совпадает
|
||||||
if (hb.blockchainId != blockchainId) {
|
// if (hb.blockchainId != blockchainId) {
|
||||||
log.warn("Несовпадение blockchainId в заголовке (ожидалось {}, получено {})",
|
// log.warn("Несовпадение blockchainId в заголовке (ожидалось {}, получено {})",
|
||||||
blockchainId, hb.blockchainId);
|
// blockchainId, hb.blockchainId);
|
||||||
return code(WireCodes.Status.BAD_REQUEST);
|
// return code(WireCodes.Status.BAD_REQUEST);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Проверяем подпись и хэш первого блока (предыдущий хэш = 0)
|
// // Проверяем подпись и хэш первого блока (предыдущий хэш = 0)
|
||||||
prevHash32 = new byte[32];
|
// prevHash32 = new byte[32];
|
||||||
boolean verified = BchCryptoVerifier.verifyAll(
|
// boolean verified = BchCryptoVerifier.verifyAll(
|
||||||
hb.userLogin,
|
// hb.userLogin,
|
||||||
blockchainId,
|
// blockchainId,
|
||||||
prevHash32,
|
// prevHash32,
|
||||||
block.rawBytes,
|
// block.rawBytes,
|
||||||
block.getSignature64(),
|
// block.getSignature64(),
|
||||||
block.getHash32(),
|
// block.getHash32(),
|
||||||
hb.publicKey32
|
// hb.publicKey32
|
||||||
);
|
// );
|
||||||
if (!verified) {
|
// if (!verified) {
|
||||||
log.warn("❌ Подпись не прошла проверку при создании цепочки blockchainId={}", blockchainId);
|
// log.warn("❌ Подпись не прошла проверку при создании цепочки blockchainId={}", blockchainId);
|
||||||
return code(WireCodes.Status.UNVERIFIED);
|
// return code(WireCodes.Status.UNVERIFIED);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// ✅ Всё хорошо: создаём новую цепочку
|
// // ✅ Всё хорошо: создаём новую цепочку
|
||||||
info.addBlockchain(blockchainId, hb.userLogin, hb.publicKey32, Integer.MAX_VALUE);
|
// info.addBlockchain(blockchainId, hb.userLogin, hb.publicKey32, Integer.MAX_VALUE);
|
||||||
info.updateBlockchainState(blockchainId, block.recordNumber, bytesToHex(block.getHash32()), fullBlock.length);
|
// info.updateBlockchainState(blockchainId, block.recordNumber, bytesToHex(block.getHash32()), fullBlock.length);
|
||||||
|
//
|
||||||
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, fullBlock);
|
// FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, fullBlock);
|
||||||
|
//
|
||||||
log.info("✅ Создана новая цепочка blockchainId={}, user={}, blockNum={}",
|
// log.info("✅ Создана новая цепочка blockchainId={}, user={}, blockNum={}",
|
||||||
blockchainId, hb.userLogin, block.recordNumber);
|
// blockchainId, hb.userLogin, block.recordNumber);
|
||||||
|
//
|
||||||
return code(WireCodes.Status.OK);
|
// return code(WireCodes.Status.OK);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
// 🧩 СЦЕНАРИЙ 2: цепочка существует — добавляем новый блок
|
// // 🧩 СЦЕНАРИЙ 2: цепочка существует — добавляем новый блок
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
expectedNum = chain.lastBlockNumber + 1;
|
// expectedNum = chain.lastBlockNumber + 1;
|
||||||
|
//
|
||||||
// Проверка последовательности (и отправка lastBlockNumber)
|
// // Проверка последовательности (и отправка lastBlockNumber)
|
||||||
if (block.recordNumber < expectedNum) {
|
// if (block.recordNumber < expectedNum) {
|
||||||
log.info("🔁 Блок {} уже существует, последний = {}", block.recordNumber, chain.lastBlockNumber);
|
// log.info("🔁 Блок {} уже существует, последний = {}", block.recordNumber, chain.lastBlockNumber);
|
||||||
ByteBuffer out = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
|
// ByteBuffer out = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
|
||||||
out.putInt(WireCodes.Status.BLOCK_ALREADY_EXISTS);
|
// out.putInt(WireCodes.Status.BLOCK_ALREADY_EXISTS);
|
||||||
out.putInt(chain.lastBlockNumber);
|
// out.putInt(chain.lastBlockNumber);
|
||||||
return out.array();
|
// return out.array();
|
||||||
}
|
// }
|
||||||
if (block.recordNumber > expectedNum) {
|
// if (block.recordNumber > expectedNum) {
|
||||||
log.warn("⚠️ Нарушена последовательность: получен {}, ожидался {}", block.recordNumber, expectedNum);
|
// log.warn("⚠️ Нарушена последовательность: получен {}, ожидался {}", block.recordNumber, expectedNum);
|
||||||
ByteBuffer out = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
|
// ByteBuffer out = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
|
||||||
out.putInt(WireCodes.Status.OUT_OF_SEQUENCE);
|
// out.putInt(WireCodes.Status.OUT_OF_SEQUENCE);
|
||||||
out.putInt(chain.lastBlockNumber);
|
// out.putInt(chain.lastBlockNumber);
|
||||||
return out.array();
|
// return out.array();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
userLogin = chain.userLogin;
|
// userLogin = chain.userLogin;
|
||||||
publicKey32 = chain.getPublicKey32();
|
// publicKey32 = chain.getPublicKey32();
|
||||||
|
//
|
||||||
// Хэш предыдущего блока (или 32 нуля, если это первый)
|
// // Хэш предыдущего блока (или 32 нуля, если это первый)
|
||||||
prevHash32 = (chain.lastBlockHash == null || chain.lastBlockHash.isEmpty())
|
// prevHash32 = (chain.lastBlockHash == null || chain.lastBlockHash.isEmpty())
|
||||||
? new byte[32]
|
// ? new byte[32]
|
||||||
: hexToBytes(chain.lastBlockHash);
|
// : hexToBytes(chain.lastBlockHash);
|
||||||
|
//
|
||||||
// Проверяем подпись и хэш
|
// // Проверяем подпись и хэш
|
||||||
boolean verified = BchCryptoVerifier.verifyAll(
|
// boolean verified = BchCryptoVerifier.verifyAll(
|
||||||
userLogin,
|
// userLogin,
|
||||||
blockchainId,
|
// blockchainId,
|
||||||
prevHash32,
|
// prevHash32,
|
||||||
block.rawBytes,
|
// block.rawBytes,
|
||||||
block.getSignature64(),
|
// block.getSignature64(),
|
||||||
block.getHash32(),
|
// block.getHash32(),
|
||||||
publicKey32
|
// publicKey32
|
||||||
);
|
// );
|
||||||
if (!verified) {
|
// if (!verified) {
|
||||||
log.warn("❌ Подпись не прошла проверку: chainId={}, blockNum={}", blockchainId, block.recordNumber);
|
// log.warn("❌ Подпись не прошла проверку: chainId={}, blockNum={}", blockchainId, block.recordNumber);
|
||||||
return code(WireCodes.Status.UNVERIFIED);
|
// return code(WireCodes.Status.UNVERIFIED);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Проверяем тело блока (например, корректный UTF-8 или структура)
|
// // Проверяем тело блока (например, корректный UTF-8 или структура)
|
||||||
BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check();
|
// BodyRecord body = BodyRecordParser.parse(block.recordType, block.recordTypeVersion, block.body).check();
|
||||||
|
//
|
||||||
// ✅ Добавляем блок в файл цепочки
|
// // ✅ Добавляем блок в файл цепочки
|
||||||
FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, fullBlock);
|
// FileStoreUtil.getInstance().addDataToBlockchain(blockchainId, fullBlock);
|
||||||
|
//
|
||||||
// Обновляем состояние цепочки (номер, хэш, размер)
|
// // Обновляем состояние цепочки (номер, хэш, размер)
|
||||||
int newSize = chain.blockchainSize + fullBlock.length;
|
// int newSize = chain.blockchainSize + fullBlock.length;
|
||||||
info.updateBlockchainState(blockchainId, block.recordNumber, bytesToHex(block.getHash32()), newSize);
|
// info.updateBlockchainState(blockchainId, block.recordNumber, bytesToHex(block.getHash32()), newSize);
|
||||||
|
//
|
||||||
log.info("✅ Блок добавлен: chain={}, num={}, type={}, bytes={}",
|
// log.info("✅ Блок добавлен: chain={}, num={}, type={}, bytes={}",
|
||||||
blockchainId, block.recordNumber, block.recordType, fullBlock.length);
|
// blockchainId, block.recordNumber, block.recordType, fullBlock.length);
|
||||||
|
//
|
||||||
return code(WireCodes.Status.OK);
|
// return code(WireCodes.Status.OK);
|
||||||
|
//
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
log.error("❌ ADD_BLOCK: внутренняя ошибка при обработке", e);
|
// log.error("❌ ADD_BLOCK: внутренняя ошибка при обработке", e);
|
||||||
return code(WireCodes.Status.INTERNAL_ERROR);
|
// return code(WireCodes.Status.INTERNAL_ERROR);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
// Утилиты
|
// // Утилиты
|
||||||
// =====================================================================
|
// // =====================================================================
|
||||||
|
//
|
||||||
/** Преобразовать статус (int) в 4 байта BigEndian. */
|
// /** Преобразовать статус (int) в 4 байта BigEndian. */
|
||||||
private static byte[] code(int status) {
|
// private static byte[] code(int status) {
|
||||||
return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(status).array();
|
// return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(status).array();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/** Конвертация HEX → bytes (для хэшей). */
|
// /** Конвертация HEX → bytes (для хэшей). */
|
||||||
private static byte[] hexToBytes(String hex) {
|
// private static byte[] hexToBytes(String hex) {
|
||||||
int len = hex.length();
|
// int len = hex.length();
|
||||||
byte[] out = new byte[len / 2];
|
// byte[] out = new byte[len / 2];
|
||||||
for (int i = 0; i < len; i += 2)
|
// for (int i = 0; i < len; i += 2)
|
||||||
out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
|
// out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
|
||||||
return out;
|
// return out;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/** Конвертация bytes → HEX (для сохранения в BchInfo). */
|
// /** Конвертация bytes → HEX (для сохранения в BchInfo). */
|
||||||
private static String bytesToHex(byte[] b) {
|
// private static String bytesToHex(byte[] b) {
|
||||||
StringBuilder sb = new StringBuilder(b.length * 2);
|
// StringBuilder sb = new StringBuilder(b.length * 2);
|
||||||
for (byte x : b) sb.append(String.format("%02x", x));
|
// for (byte x : b) sb.append(String.format("%02x", x));
|
||||||
return sb.toString();
|
// return sb.toString();
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
|
|||||||
76
src/main/java/test/it/WsTestClient.java
Normal file
76
src/main/java/test/it/WsTestClient.java
Normal file
@ -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<String, CompletableFuture<String>> 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<String> 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<String> 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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/test/java/test/it/AddUserIT.java
Normal file
34
src/test/java/test/it/AddUserIT.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/test/java/test/it/JsonBuilders.java
Normal file
111
src/test/java/test/it/JsonBuilders.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/test/java/test/it/JsonParsers.java
Normal file
82
src/test/java/test/it/JsonParsers.java
Normal file
@ -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<String> sessionIds(String json) {
|
||||||
|
List<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/test/java/test/it/SessionsIT.java
Normal file
166
src/test/java/test/it/SessionsIT.java
Normal file
@ -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<String> 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<String> 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<String> 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<String> ids = JsonParsers.sessionIds(resp2);
|
||||||
|
assertTrue(ids.isEmpty(), "Sessions must be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/test/java/test/it/TestConfig.java
Normal file
37
src/test/java/test/it/TestConfig.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user