SHA256
Solana-first регистрация: lazy-import пользователя при входе, AddUser отключен, UI ожидание 15с
This commit is contained in:
+20
-1
@@ -11,6 +11,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
@@ -30,6 +32,7 @@ import java.security.SecureRandom;
|
||||
*/
|
||||
public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_AuthChallenge_Handler.class);
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
@Override
|
||||
@@ -58,6 +61,22 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||
if (solanaUserEntry == null) {
|
||||
try {
|
||||
solanaUserEntry = SolanaUserPdaImportService.findOrImportByLogin(login);
|
||||
if (solanaUserEntry != null) {
|
||||
log.info("AuthChallenge: пользователь {} импортирован из Solana PDA", solanaUserEntry.getLogin());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("AuthChallenge: ошибка lazy-import пользователя {} из Solana", login, e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"SOLANA_IMPORT_FAILED",
|
||||
"Ошибка проверки пользователя в Solana"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (solanaUserEntry == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@@ -84,4 +103,4 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.UserCreateDAO;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import utils.config.SolanaProgramsConfig;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Lazy-import пользователя из Solana PDA в локальную БД сервера.
|
||||
* Используется при входе, если в solana_users нет записи по login.
|
||||
*/
|
||||
public final class SolanaUserPdaImportService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SolanaUserPdaImportService.class);
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final HttpClient HTTP = HttpClient.newHttpClient();
|
||||
private static final int USER_PDA_SPACE = 768;
|
||||
private static final String MAGIC = "SHiNE";
|
||||
|
||||
private SolanaUserPdaImportService() {}
|
||||
|
||||
public static SolanaUserEntry findOrImportByLogin(String loginRaw) throws Exception {
|
||||
String login = normalizeLogin(loginRaw);
|
||||
if (login == null) return null;
|
||||
|
||||
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
||||
SolanaUserEntry existing = usersDao.getByLogin(login);
|
||||
if (existing != null) return existing;
|
||||
|
||||
ParsedSolanaUser parsed = fetchFromSolana(login);
|
||||
if (parsed == null) return null;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long sizeLimit = parsed.paidLimitBytes > 0 ? parsed.paidLimitBytes : 100_000L;
|
||||
boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain(
|
||||
parsed.login,
|
||||
parsed.blockchainName,
|
||||
parsed.deviceKeyB64, // в текущей модели solanaKey = deviceKey
|
||||
parsed.blockchainKeyB64,
|
||||
parsed.deviceKeyB64,
|
||||
sizeLimit,
|
||||
now
|
||||
);
|
||||
if (!inserted) {
|
||||
return usersDao.getByLogin(login);
|
||||
}
|
||||
return usersDao.getByLogin(login);
|
||||
}
|
||||
|
||||
private static ParsedSolanaUser fetchFromSolana(String login) throws Exception {
|
||||
String loginB58 = toBase58(login.getBytes(StandardCharsets.UTF_8));
|
||||
String lenB58 = toBase58(new byte[]{(byte) login.length()});
|
||||
|
||||
String reqJson = """
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"id":1,
|
||||
"method":"getProgramAccounts",
|
||||
"params":[
|
||||
"%s",
|
||||
{
|
||||
"encoding":"base64",
|
||||
"filters":[
|
||||
{"dataSize":%d},
|
||||
{"memcmp":{"offset":61,"bytes":"%s"}},
|
||||
{"memcmp":{"offset":62,"bytes":"%s"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(SolanaProgramsConfig.SHINE_USERS_PROGRAM_ID, USER_PDA_SPACE, lenB58, loginB58);
|
||||
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SolanaProgramsConfig.SOLANA_RPC_URL))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(reqJson))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
|
||||
throw new IllegalStateException("Solana RPC HTTP status=" + resp.statusCode());
|
||||
}
|
||||
|
||||
JsonNode root = MAPPER.readTree(resp.body());
|
||||
if (root.hasNonNull("error")) {
|
||||
throw new IllegalStateException("Solana RPC error: " + root.get("error").toString());
|
||||
}
|
||||
|
||||
JsonNode result = root.path("result");
|
||||
if (!result.isArray() || result.isEmpty()) return null;
|
||||
|
||||
for (JsonNode item : result) {
|
||||
JsonNode dataNode = item.path("account").path("data");
|
||||
if (!dataNode.isArray() || dataNode.size() < 1) continue;
|
||||
String b64 = dataNode.get(0).asText("");
|
||||
if (b64.isBlank()) continue;
|
||||
byte[] raw = Base64.getDecoder().decode(b64);
|
||||
ParsedSolanaUser parsed = parseUserPda(raw);
|
||||
if (parsed != null && parsed.login.equalsIgnoreCase(login)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ParsedSolanaUser parseUserPda(byte[] raw) {
|
||||
if (raw == null || raw.length < 128) return null;
|
||||
if (!MAGIC.equals(new String(raw, 0, 5, StandardCharsets.UTF_8))) return null;
|
||||
|
||||
int recordLen = u16le(raw, 7);
|
||||
if (recordLen < 73 || recordLen > raw.length) return null;
|
||||
|
||||
int c = 9;
|
||||
c += 8; // created_at_ms
|
||||
c += 8; // updated_at_ms
|
||||
c += 4; // record_number
|
||||
c += 32; // prev_record_hash
|
||||
|
||||
int loginLen = u8(raw, c++);
|
||||
if (loginLen <= 0 || c + loginLen > recordLen) return null;
|
||||
String login = new String(raw, c, loginLen, StandardCharsets.UTF_8);
|
||||
c += loginLen;
|
||||
|
||||
int blocksCount = u8(raw, c++);
|
||||
String blockchainName = null;
|
||||
byte[] blockchainKey32 = null;
|
||||
byte[] deviceKey32 = null;
|
||||
long paidLimitBytes = 0L;
|
||||
|
||||
for (int i = 0; i < blocksCount; i++) {
|
||||
int blockType = u8(raw, c++);
|
||||
int blockVer = u8(raw, c++);
|
||||
if (blockVer != 0) return null;
|
||||
|
||||
if (blockType == 1) {
|
||||
c += 32;
|
||||
} else if (blockType == 2) {
|
||||
deviceKey32 = slice(raw, c, 32);
|
||||
c += 32;
|
||||
} else if (blockType == 3) {
|
||||
int count = u8(raw, c++);
|
||||
for (int j = 0; j < count; j++) {
|
||||
c += 1; // blockchain_type
|
||||
int bchLen = u8(raw, c++);
|
||||
blockchainName = new String(raw, c, bchLen, StandardCharsets.UTF_8);
|
||||
c += bchLen;
|
||||
blockchainKey32 = slice(raw, c, 32);
|
||||
c += 32;
|
||||
paidLimitBytes = u64le(raw, c);
|
||||
c += 8;
|
||||
c += 8; // used_bytes
|
||||
c += 4; // last_block_number
|
||||
c += 32; // last_block_hash
|
||||
c += 64; // last_block_signature
|
||||
int arweavePresent = u8(raw, c++);
|
||||
if (arweavePresent == 1) {
|
||||
int arLen = u8(raw, c++);
|
||||
c += arLen;
|
||||
} else if (arweavePresent != 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if (blockType == 4) {
|
||||
c += 1 + 32;
|
||||
int addrLen = u8(raw, c++);
|
||||
c += addrLen;
|
||||
int syncCount = u8(raw, c++);
|
||||
for (int j = 0; j < syncCount; j++) {
|
||||
int n = u8(raw, c++);
|
||||
c += n;
|
||||
}
|
||||
} else if (blockType == 40) {
|
||||
int accessCount = u8(raw, c++);
|
||||
for (int j = 0; j < accessCount; j++) {
|
||||
int n = u8(raw, c++);
|
||||
c += n;
|
||||
}
|
||||
} else if (blockType == 50) {
|
||||
c += 1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (c > recordLen) return null;
|
||||
}
|
||||
|
||||
if (blockchainName == null || blockchainKey32 == null || deviceKey32 == null) return null;
|
||||
return new ParsedSolanaUser(
|
||||
login,
|
||||
blockchainName,
|
||||
Base64.getEncoder().encodeToString(blockchainKey32),
|
||||
Base64.getEncoder().encodeToString(deviceKey32),
|
||||
paidLimitBytes
|
||||
);
|
||||
}
|
||||
|
||||
private static String normalizeLogin(String login) {
|
||||
if (login == null) return null;
|
||||
String s = login.trim();
|
||||
if (s.isEmpty()) return null;
|
||||
return s.toLowerCase();
|
||||
}
|
||||
|
||||
private static int u8(byte[] b, int o) { return b[o] & 0xFF; }
|
||||
private static int u16le(byte[] b, int o) {
|
||||
return (b[o] & 0xFF) | ((b[o + 1] & 0xFF) << 8);
|
||||
}
|
||||
private static long u64le(byte[] b, int o) {
|
||||
long out = 0L;
|
||||
for (int i = 0; i < 8; i++) out |= ((long) (b[o + i] & 0xFF)) << (8 * i);
|
||||
return out;
|
||||
}
|
||||
private static byte[] slice(byte[] b, int o, int len) {
|
||||
byte[] out = new byte[len];
|
||||
System.arraycopy(b, o, out, 0, len);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Нужен для memcmp.bytes в Solana RPC.
|
||||
private static String toBase58(byte[] input) {
|
||||
final char[] alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
|
||||
int zeros = 0;
|
||||
while (zeros < input.length && input[zeros] == 0) zeros++;
|
||||
byte[] tmp = new byte[input.length * 2];
|
||||
int j = tmp.length;
|
||||
int startAt = zeros;
|
||||
while (startAt < input.length) {
|
||||
int mod = divmod58(input, startAt);
|
||||
if (input[startAt] == 0) startAt++;
|
||||
tmp[--j] = (byte) alphabet[mod];
|
||||
}
|
||||
while (j < tmp.length && tmp[j] == alphabet[0]) j++;
|
||||
while (--zeros >= 0) tmp[--j] = (byte) alphabet[0];
|
||||
return new String(tmp, j, tmp.length - j, StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private static int divmod58(byte[] number, int startAt) {
|
||||
int remainder = 0;
|
||||
for (int i = startAt; i < number.length; i++) {
|
||||
int digit256 = number[i] & 0xFF;
|
||||
int temp = remainder * 256 + digit256;
|
||||
number[i] = (byte) (temp / 58);
|
||||
remainder = temp % 58;
|
||||
}
|
||||
return remainder;
|
||||
}
|
||||
|
||||
private record ParsedSolanaUser(
|
||||
String login,
|
||||
String blockchainName,
|
||||
String blockchainKeyB64,
|
||||
String deviceKeyB64,
|
||||
long paidLimitBytes
|
||||
) {}
|
||||
}
|
||||
|
||||
+10
-169
@@ -1,185 +1,26 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.tempToTest;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.Base64Ws;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.BlockchainStateDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import utils.blockchain.BlockchainNameUtil;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* AddUser отключен: регистрация работает через Solana.
|
||||
*/
|
||||
public class Net_AddUser_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
|
||||
|
||||
/** TEST ONLY */
|
||||
private static final int TEST_BCH_LIMIT = 1_000_000;
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
|
||||
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||
|| req.getBlockchainName() == null || req.getBlockchainName().isBlank()
|
||||
|| req.getSolanaKey() == null || req.getSolanaKey().isBlank()
|
||||
|| req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
|
||||
|| req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
|
||||
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_FIELDS",
|
||||
"Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
|
||||
);
|
||||
}
|
||||
|
||||
// blockchainName должен быть вида: <login>-NNN
|
||||
if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BLOCKCHAIN_NAME",
|
||||
"blockchainName должен быть вида <login>-NNN (пример: anya-001)"
|
||||
);
|
||||
}
|
||||
|
||||
int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0)
|
||||
? TEST_BCH_LIMIT
|
||||
: req.getBchLimit();
|
||||
|
||||
try {
|
||||
// базовая валидация форматов ключей: Base64(32 bytes)
|
||||
byte[] solanaKey32;
|
||||
byte[] blockchainKey32;
|
||||
byte[] deviceKey32;
|
||||
|
||||
try {
|
||||
solanaKey32 = Base64Ws.decodeLen(req.getSolanaKey(), 32, "solanaKey");
|
||||
blockchainKey32 = Base64Ws.decodeLen(req.getBlockchainKey(), 32, "blockchainKey");
|
||||
deviceKey32 = Base64Ws.decodeLen(req.getDeviceKey(), 32, "deviceKey");
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_KEY_FORMAT",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
// (переменные не используются дальше, но оставляем для ясности проверки длины)
|
||||
if (solanaKey32.length != 32 || blockchainKey32.length != 32 || deviceKey32.length != 32) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_KEY_FORMAT",
|
||||
"solanaKey/blockchainKey/deviceKey должны быть Base64(32 bytes)"
|
||||
);
|
||||
}
|
||||
|
||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
||||
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
c.setAutoCommit(false);
|
||||
|
||||
// 1. Проверяем, что пользователя нет (case-insensitive)
|
||||
if (usersDAO.getByLogin(c, req.getLogin()) != null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
409,
|
||||
"USER_ALREADY_EXISTS",
|
||||
"Пользователь с таким login уже существует"
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД)
|
||||
if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
409,
|
||||
"BLOCKCHAIN_ALREADY_EXISTS",
|
||||
"Пользователь с таким blockchainName уже существует"
|
||||
);
|
||||
}
|
||||
|
||||
// 3. На всякий случай оставляем старую проверку blockchain_state,
|
||||
// потому что эта таблица нужна серверу (состояние цепочки/лимиты).
|
||||
if (stateDAO.getByBlockchainName(c, req.getBlockchainName()) != null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
409,
|
||||
"BLOCKCHAIN_STATE_ALREADY_EXISTS",
|
||||
"blockchain_state уже существует"
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Создаём пользователя (все поля теперь лежат в solana_users)
|
||||
SolanaUserEntry user = new SolanaUserEntry();
|
||||
user.setLogin(req.getLogin());
|
||||
user.setBlockchainName(req.getBlockchainName());
|
||||
user.setSolanaKey(req.getSolanaKey());
|
||||
user.setBlockchainKey(req.getBlockchainKey());
|
||||
user.setDeviceKey(req.getDeviceKey());
|
||||
|
||||
usersDAO.insert(c, user);
|
||||
|
||||
// 5. Создаём INITIAL blockchain_state (для работы сервера)
|
||||
BlockchainStateEntry st = new BlockchainStateEntry();
|
||||
st.setBlockchainName(req.getBlockchainName());
|
||||
st.setLogin(req.getLogin());
|
||||
st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
|
||||
st.setLastBlockNumber(-1);
|
||||
st.setLastBlockHash(new byte[32]);
|
||||
st.setFileSizeBytes(0);
|
||||
st.setSizeLimit(limit);
|
||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
||||
|
||||
stateDAO.upsert(c, st);
|
||||
|
||||
c.commit();
|
||||
}
|
||||
|
||||
Net_AddUser_Response resp = new Net_AddUser_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
|
||||
req.getLogin(), req.getBlockchainName(), limit);
|
||||
|
||||
return resp;
|
||||
|
||||
} catch (SQLException e) {
|
||||
log.error("❌ DB error AddUser", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR",
|
||||
"Ошибка БД"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Internal error AddUser", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.INTERNAL_ERROR,
|
||||
"INTERNAL_ERROR",
|
||||
NetExceptionResponseFactory.detailedMessage("Внутренняя ошибка сервера при AddUser", e)
|
||||
);
|
||||
}
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
410,
|
||||
"ADD_USER_DISABLED",
|
||||
"Серверная регистрация AddUser отключена. Используйте регистрацию через Solana."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user