SHA256
Миграция PDA на client.key
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
@@ -140,7 +140,7 @@ public final class DatabaseInitializer {
|
||||
// 1. solana_users
|
||||
// ВАЖНО:
|
||||
// - Все требуемые поля теперь лежат в solana_users:
|
||||
// login, blockchain_name, solana_key, blockchain_key, device_key
|
||||
// login, blockchain_name, solana_key, blockchain_key, client_key
|
||||
// - Поиск по login в DAO сделан case-insensitive.
|
||||
// - Для защиты от дублей "Anya" и "anya" добавляем COLLATE NOCASE на PRIMARY KEY.
|
||||
st.executeUpdate("""
|
||||
@@ -149,7 +149,7 @@ public final class DatabaseInitializer {
|
||||
blockchain_name TEXT NOT NULL,
|
||||
solana_key TEXT NOT NULL,
|
||||
blockchain_key TEXT NOT NULL,
|
||||
device_key TEXT NOT NULL
|
||||
client_key TEXT NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
@@ -238,7 +238,7 @@ public final class DatabaseInitializer {
|
||||
param TEXT NOT NULL,
|
||||
time_ms INTEGER NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
device_key TEXT,
|
||||
client_key TEXT,
|
||||
signature TEXT,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
UNIQUE (login, param)
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.List;
|
||||
* - blockchain_name TEXT NOT NULL
|
||||
* - solana_key TEXT NOT NULL
|
||||
* - blockchain_key TEXT NOT NULL
|
||||
* - device_key TEXT NOT NULL
|
||||
* - client_key TEXT NOT NULL
|
||||
*
|
||||
* Правило работы с соединениями:
|
||||
* - методы с Connection НЕ закрывают соединение
|
||||
@@ -45,7 +45,7 @@ public final class SolanaUsersDAO {
|
||||
public void insert(Connection c, SolanaUserEntry user) throws SQLException {
|
||||
String sql = """
|
||||
INSERT INTO solana_users (
|
||||
login, blockchain_name, solana_key, blockchain_key, device_key
|
||||
login, blockchain_name, solana_key, blockchain_key, client_key
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
@@ -54,7 +54,7 @@ public final class SolanaUsersDAO {
|
||||
ps.setString(2, user.getBlockchainName());
|
||||
ps.setString(3, user.getSolanaKey());
|
||||
ps.setString(4, user.getBlockchainKey());
|
||||
ps.setString(5, user.getDeviceKey());
|
||||
ps.setString(5, user.getClientKey());
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ public final class SolanaUsersDAO {
|
||||
blockchain_name,
|
||||
solana_key,
|
||||
blockchain_key,
|
||||
device_key
|
||||
client_key
|
||||
FROM solana_users
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
""";
|
||||
@@ -155,7 +155,7 @@ public final class SolanaUsersDAO {
|
||||
blockchain_name,
|
||||
solana_key,
|
||||
blockchain_key,
|
||||
device_key
|
||||
client_key
|
||||
FROM solana_users
|
||||
WHERE blockchain_name = ?
|
||||
""";
|
||||
@@ -184,7 +184,7 @@ public final class SolanaUsersDAO {
|
||||
blockchain_name,
|
||||
solana_key,
|
||||
blockchain_key,
|
||||
device_key
|
||||
client_key
|
||||
FROM solana_users
|
||||
WHERE LOWER(login) LIKE ?
|
||||
ORDER BY login
|
||||
@@ -219,7 +219,7 @@ public final class SolanaUsersDAO {
|
||||
e.setBlockchainName(rs.getString("blockchain_name"));
|
||||
e.setSolanaKey(rs.getString("solana_key"));
|
||||
e.setBlockchainKey(rs.getString("blockchain_key"));
|
||||
e.setDeviceKey(rs.getString("device_key"));
|
||||
e.setClientKey(rs.getString("client_key"));
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import java.sql.*;
|
||||
|
||||
/**
|
||||
* UserCreateDAO — атомарное добавление пользователя:
|
||||
* - solana_users (login, blockchain_name, solana_key, blockchain_key, device_key)
|
||||
* - solana_users (login, blockchain_name, solana_key, blockchain_key, client_key)
|
||||
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
|
||||
*
|
||||
* ВАЖНО:
|
||||
@@ -39,7 +39,7 @@ public final class UserCreateDAO {
|
||||
String blockchainName,
|
||||
String solanaKey,
|
||||
String blockchainKey,
|
||||
String deviceKey,
|
||||
String clientKey,
|
||||
long sizeLimit,
|
||||
long nowMs
|
||||
) throws SQLException {
|
||||
@@ -55,7 +55,7 @@ public final class UserCreateDAO {
|
||||
u.setBlockchainName(blockchainName);
|
||||
u.setSolanaKey(solanaKey);
|
||||
u.setBlockchainKey(blockchainKey);
|
||||
u.setDeviceKey(deviceKey);
|
||||
u.setClientKey(clientKey);
|
||||
|
||||
usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint
|
||||
|
||||
|
||||
@@ -43,14 +43,14 @@ public final class UserParamsDAO {
|
||||
param,
|
||||
time_ms,
|
||||
value,
|
||||
device_key,
|
||||
client_key,
|
||||
signature
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(login, param)
|
||||
DO UPDATE SET
|
||||
time_ms = excluded.time_ms,
|
||||
value = excluded.value,
|
||||
device_key = excluded.device_key,
|
||||
client_key = excluded.client_key,
|
||||
signature = excluded.signature
|
||||
WHERE users_params.time_ms < excluded.time_ms
|
||||
""";
|
||||
@@ -61,7 +61,7 @@ public final class UserParamsDAO {
|
||||
ps.setLong(3, e.getTimeMs());
|
||||
ps.setString(4, e.getValue());
|
||||
|
||||
if (e.getDeviceKey() != null) ps.setString(5, e.getDeviceKey());
|
||||
if (e.getClientKey() != null) ps.setString(5, e.getClientKey());
|
||||
else ps.setNull(5, Types.VARCHAR);
|
||||
|
||||
if (e.getSignature() != null) ps.setString(6, e.getSignature());
|
||||
@@ -86,7 +86,7 @@ public final class UserParamsDAO {
|
||||
param,
|
||||
time_ms,
|
||||
value,
|
||||
device_key,
|
||||
client_key,
|
||||
signature
|
||||
FROM users_params
|
||||
WHERE login = ? COLLATE NOCASE AND param = ?
|
||||
@@ -117,7 +117,7 @@ public final class UserParamsDAO {
|
||||
param,
|
||||
time_ms,
|
||||
value,
|
||||
device_key,
|
||||
client_key,
|
||||
signature
|
||||
FROM users_params
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
@@ -149,9 +149,9 @@ public final class UserParamsDAO {
|
||||
e.setTimeMs(rs.getLong("time_ms"));
|
||||
e.setValue(rs.getString("value"));
|
||||
|
||||
String dk = rs.getString("device_key");
|
||||
String dk = rs.getString("client_key");
|
||||
if (rs.wasNull()) dk = null;
|
||||
e.setDeviceKey(dk);
|
||||
e.setClientKey(dk);
|
||||
|
||||
String sig = rs.getString("signature");
|
||||
if (rs.wasNull()) sig = null;
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.util.Base64;
|
||||
* - blockchain_name — TEXT NOT NULL
|
||||
* - solana_key — TEXT NOT NULL
|
||||
* - blockchain_key — TEXT NOT NULL
|
||||
* - device_key — TEXT NOT NULL
|
||||
* - client_key — TEXT NOT NULL
|
||||
*/
|
||||
public class SolanaUserEntry {
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SolanaUserEntry {
|
||||
private String blockchainKey;
|
||||
|
||||
/** Ключ устройства (публичный ключ устройства) */
|
||||
private String deviceKey;
|
||||
private String clientKey;
|
||||
|
||||
public SolanaUserEntry() {}
|
||||
|
||||
@@ -35,12 +35,12 @@ public class SolanaUserEntry {
|
||||
String blockchainName,
|
||||
String solanaKey,
|
||||
String blockchainKey,
|
||||
String deviceKey) {
|
||||
String clientKey) {
|
||||
this.login = login;
|
||||
this.blockchainName = blockchainName;
|
||||
this.solanaKey = solanaKey;
|
||||
this.blockchainKey = blockchainKey;
|
||||
this.deviceKey = deviceKey;
|
||||
this.clientKey = clientKey;
|
||||
}
|
||||
|
||||
public String getLogin() { return login; }
|
||||
@@ -55,13 +55,13 @@ public class SolanaUserEntry {
|
||||
public String getBlockchainKey() { return blockchainKey; }
|
||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||
|
||||
public String getDeviceKey() { return deviceKey; }
|
||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
||||
public String getClientKey() { return clientKey; }
|
||||
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||
|
||||
// оставляю этот метод как утилиту (иногда удобно), но он работает только для deviceKey:
|
||||
public byte[] getDeviceKeyByte() {
|
||||
if (deviceKey == null) return null;
|
||||
String s = deviceKey.trim();
|
||||
// оставляю этот метод как утилиту (иногда удобно), но он работает только для clientKey:
|
||||
public byte[] getClientKeyByte() {
|
||||
if (clientKey == null) return null;
|
||||
String s = clientKey.trim();
|
||||
if (s.isEmpty()) return null;
|
||||
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,7 @@ package shine.db.entities;
|
||||
* - param TEXT NOT NULL
|
||||
* - time_ms INTEGER NOT NULL
|
||||
* - value TEXT NOT NULL
|
||||
* - device_key TEXT NULL
|
||||
* - client_key TEXT NULL
|
||||
* - signature TEXT NULL
|
||||
*/
|
||||
public class UserParamEntry {
|
||||
@@ -18,17 +18,17 @@ public class UserParamEntry {
|
||||
private long timeMs;
|
||||
private String value;
|
||||
|
||||
private String deviceKey;
|
||||
private String clientKey;
|
||||
private String signature;
|
||||
|
||||
public UserParamEntry() {}
|
||||
|
||||
public UserParamEntry(String login, String param, long timeMs, String value, String deviceKey, String signature) {
|
||||
public UserParamEntry(String login, String param, long timeMs, String value, String clientKey, String signature) {
|
||||
this.login = login;
|
||||
this.param = param;
|
||||
this.timeMs = timeMs;
|
||||
this.value = value;
|
||||
this.deviceKey = deviceKey;
|
||||
this.clientKey = clientKey;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ public class UserParamEntry {
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
|
||||
public String getDeviceKey() { return deviceKey; }
|
||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
||||
public String getClientKey() { return clientKey; }
|
||||
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||
|
||||
public String getSignature() { return signature; }
|
||||
public void setSignature(String signature) { this.signature = signature; }
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
+2
-2
@@ -10,7 +10,7 @@ import shine.db.entities.ActiveSessionEntry;
|
||||
*
|
||||
* Важно (v2):
|
||||
* - Авторизация всегда 2 шага:
|
||||
* A) Создание новой сессии через deviceKey:
|
||||
* A) Создание новой сессии через clientKey:
|
||||
* AuthChallenge(login) -> ctx.authNonce
|
||||
* CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
|
||||
*
|
||||
@@ -39,7 +39,7 @@ public class ConnectionContext {
|
||||
|
||||
/**
|
||||
* Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
|
||||
* используется на шаге CreateAuthSession для проверки подписи deviceKey.
|
||||
* используется на шаге CreateAuthSession для проверки подписи clientKey.
|
||||
*/
|
||||
private String authNonce;
|
||||
|
||||
|
||||
-4548
File diff suppressed because it is too large
Load Diff
-20
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
package server.logic.ws_protocol.JSON.entyties;
|
||||
|
||||
/**
|
||||
* Базовый класс для всех событий (event).
|
||||
* Общие поля: op и payload.
|
||||
*.
|
||||
* Формат JSON (event):
|
||||
* {
|
||||
* "op": "...",
|
||||
* "payload": { ... }
|
||||
* }
|
||||
*/
|
||||
public abstract class Net_Event {
|
||||
|
||||
/** Имя операции / события (op). */
|
||||
private String op;
|
||||
|
||||
/**
|
||||
* Произвольные данные.
|
||||
* В JSON это поле "payload".
|
||||
*/
|
||||
private Object payload;
|
||||
|
||||
// --- getters / setters ---
|
||||
|
||||
public String getOp() {
|
||||
return op;
|
||||
}
|
||||
|
||||
public void setOp(String op) {
|
||||
this.op = op;
|
||||
}
|
||||
|
||||
public Object getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void setPayload(Object payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
package server.logic.ws_protocol.JSON.entyties;
|
||||
|
||||
/**
|
||||
* Ответ с ошибкой (любой отказ).
|
||||
*.
|
||||
* В payload будет:
|
||||
* {
|
||||
* "code": "...",
|
||||
* "message": "..."
|
||||
* }
|
||||
*/
|
||||
public class Net_Exception_Response extends Net_Response {
|
||||
|
||||
private String code;
|
||||
private String message;
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
package server.logic.ws_protocol.JSON.entyties;
|
||||
|
||||
/**
|
||||
* Базовый класс для всех запросов (client → server).
|
||||
*.
|
||||
* Наследуется от NetEvent и добавляет requestId.
|
||||
*.
|
||||
* Формат JSON (request):
|
||||
* {
|
||||
* "op": "...",
|
||||
* "requestId": "...",
|
||||
* "payload": { ... }
|
||||
* }
|
||||
*/
|
||||
public abstract class Net_Request extends Net_Event {
|
||||
|
||||
/** Идентификатор запроса, чтобы связать запрос и ответ. */
|
||||
private String requestId;
|
||||
|
||||
// --- getters / setters ---
|
||||
|
||||
public String getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public void setRequestId(String requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
}
|
||||
|
||||
package server.logic.ws_protocol.JSON.entyties;
|
||||
|
||||
/**
|
||||
* Базовый класс для всех ответов (server → client).
|
||||
*.
|
||||
* Наследуется от NetRequest и добавляет status.
|
||||
*.
|
||||
* Формат JSON (response):
|
||||
* {
|
||||
* "op": "...",
|
||||
* "requestId": "...",
|
||||
* "status": 200,
|
||||
* "payload": { ... } // и для успеха, и для ошибки
|
||||
* }
|
||||
*/
|
||||
public abstract class Net_Response extends Net_Request {
|
||||
|
||||
/** Статус результата (200 — успех, любое другое значение — ошибка). */
|
||||
private int status;
|
||||
|
||||
// --- getters / setters ---
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(int status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return status == 200;
|
||||
}
|
||||
}
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
-3475
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -20,9 +20,9 @@ import java.security.SecureRandom;
|
||||
* AuthChallenge (v2) — шаг 1 создания новой сессии.
|
||||
*
|
||||
* Логика авторизации (v2):
|
||||
* - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
|
||||
* - Создание новой сессии возможно ТОЛЬКО через clientKey пользователя.
|
||||
* - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
|
||||
* CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
|
||||
* CreateAuthSession(..., signature(clientKey, AUTH_CREATE_SESSION:...))
|
||||
*
|
||||
* Что делает:
|
||||
* 1) Проверяет login.
|
||||
|
||||
+20
-20
@@ -30,7 +30,7 @@ import java.security.SecureRandom;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
|
||||
* CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО clientKey).
|
||||
*
|
||||
* Логика авторизации (v2):
|
||||
* - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
|
||||
@@ -38,7 +38,7 @@ import java.sql.SQLException;
|
||||
* отправляет на сервер sessionKey целиком одной строкой.
|
||||
* - Сервер сохраняет sessionKey в active_sessions.session_key как есть.
|
||||
*
|
||||
* Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
|
||||
* Подпись clientKey (Ed25519) проверяется над строкой (UTF-8):
|
||||
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||
*
|
||||
* На выходе:
|
||||
@@ -226,15 +226,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
}
|
||||
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getClientPlatform());
|
||||
|
||||
String deviceKeyFromDb = user.getDeviceKey();
|
||||
if (deviceKeyFromDb == null || deviceKeyFromDb.isBlank()) {
|
||||
String clientKeyFromDb = user.getClientKey();
|
||||
if (clientKeyFromDb == null || clientKeyFromDb.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"NO_DEVICE_KEY",
|
||||
"Отсутствует deviceKey у пользователя"
|
||||
"Отсутствует clientKey у пользователя"
|
||||
);
|
||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: no deviceKey");
|
||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: no clientKey");
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -261,28 +261,28 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
String deviceKeyFromReq = req.getDeviceKey();
|
||||
if (deviceKeyFromReq == null || deviceKeyFromReq.isBlank()) {
|
||||
String clientKeyFromReq = req.getClientKey();
|
||||
if (clientKeyFromReq == null || clientKeyFromReq.isBlank()) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"EMPTY_DEVICE_KEY",
|
||||
"Пустой deviceKey"
|
||||
"Пустой clientKey"
|
||||
);
|
||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: empty deviceKey");
|
||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: empty clientKey");
|
||||
return err;
|
||||
}
|
||||
deviceKeyFromReq = deviceKeyFromReq.trim();
|
||||
clientKeyFromReq = clientKeyFromReq.trim();
|
||||
|
||||
// TODO: для ротации device_key стоит дополнительно сверять актуальное значение через Solana.
|
||||
if (!deviceKeyFromReq.equals(deviceKeyFromDb)) {
|
||||
// TODO: для ротации client_key стоит дополнительно сверять актуальное значение через Solana.
|
||||
if (!clientKeyFromReq.equals(clientKeyFromDb)) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"DEVICE_KEY_NOT_ACTUAL",
|
||||
"device_key не соответствует актуальной версии"
|
||||
"client_key не соответствует актуальной версии"
|
||||
);
|
||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: device key mismatch");
|
||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: client key mismatch");
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
storagePwd,
|
||||
authNonce,
|
||||
timeMs,
|
||||
deviceKeyFromDb,
|
||||
clientKeyFromDb,
|
||||
signatureB64
|
||||
);
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
@@ -302,9 +302,9 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
req,
|
||||
422,
|
||||
"UNSUPPORTED_KEY_ALGORITHM",
|
||||
"deviceKey algorithm is not supported"
|
||||
"clientKey algorithm is not supported"
|
||||
);
|
||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: unsupported device key algorithm");
|
||||
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: unsupported client key algorithm");
|
||||
return err;
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
@@ -440,11 +440,11 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
String storagePwd,
|
||||
String authNonce,
|
||||
long timeMs,
|
||||
String deviceKey,
|
||||
String clientKey,
|
||||
String signatureB64
|
||||
) throws IllegalArgumentException {
|
||||
|
||||
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(deviceKey, "deviceKey");
|
||||
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(clientKey, "clientKey");
|
||||
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
|
||||
|
||||
String preimageStr = "AUTH_CREATE_SESSION:"
|
||||
|
||||
+7
-7
@@ -48,9 +48,9 @@ public final class SolanaUserPdaImportService {
|
||||
boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain(
|
||||
parsed.login,
|
||||
parsed.blockchainName,
|
||||
parsed.deviceKeyB64, // в текущей модели solanaKey = deviceKey
|
||||
parsed.clientKeyB64, // в текущей модели solanaKey = clientKey
|
||||
parsed.blockchainKeyB64,
|
||||
parsed.deviceKeyB64,
|
||||
parsed.clientKeyB64,
|
||||
sizeLimit,
|
||||
now
|
||||
);
|
||||
@@ -158,7 +158,7 @@ public final class SolanaUserPdaImportService {
|
||||
int blocksCount = u8(raw, c++);
|
||||
String blockchainName = null;
|
||||
byte[] blockchainKey32 = null;
|
||||
byte[] deviceKey32 = null;
|
||||
byte[] clientKey32 = null;
|
||||
long paidLimitBytes = 0L;
|
||||
List<ParsedSessionRecord> sessions = new ArrayList<>();
|
||||
|
||||
@@ -170,7 +170,7 @@ public final class SolanaUserPdaImportService {
|
||||
if (blockType == 1) {
|
||||
c += 32;
|
||||
} else if (blockType == 2) {
|
||||
deviceKey32 = slice(raw, c, 32);
|
||||
clientKey32 = slice(raw, c, 32);
|
||||
c += 32;
|
||||
} else if (blockType == 3) {
|
||||
int count = u8(raw, c++);
|
||||
@@ -245,12 +245,12 @@ public final class SolanaUserPdaImportService {
|
||||
if (c > recordLen) return null;
|
||||
}
|
||||
|
||||
if (blockchainName == null || blockchainKey32 == null || deviceKey32 == null) return null;
|
||||
if (blockchainName == null || blockchainKey32 == null || clientKey32 == null) return null;
|
||||
return new ParsedSolanaUser(
|
||||
login,
|
||||
blockchainName,
|
||||
Base64.getEncoder().encodeToString(blockchainKey32),
|
||||
Base64.getEncoder().encodeToString(deviceKey32),
|
||||
Base64.getEncoder().encodeToString(clientKey32),
|
||||
paidLimitBytes,
|
||||
sessions
|
||||
);
|
||||
@@ -318,7 +318,7 @@ public final class SolanaUserPdaImportService {
|
||||
String login,
|
||||
String blockchainName,
|
||||
String blockchainKeyB64,
|
||||
String deviceKeyB64,
|
||||
String clientKeyB64,
|
||||
long paidLimitBytes,
|
||||
List<ParsedSessionRecord> sessions
|
||||
) {}
|
||||
|
||||
-1439
File diff suppressed because it is too large
Load Diff
-20
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
+5
-5
@@ -35,7 +35,7 @@
|
||||
|
||||
1. Добавление пользователя (AddUser)
|
||||
|
||||
Назначение: создать локальную запись пользователя с двумя ключами — solanaKey и deviceKey.
|
||||
Назначение: создать локальную запись пользователя с двумя ключами — solanaKey и clientKey.
|
||||
|
||||
📤 Запрос клиента
|
||||
{
|
||||
@@ -46,7 +46,7 @@
|
||||
"loginId": 100212,
|
||||
"bchId": 4222,
|
||||
"solanaKey": "BASE64_LOGIN_KEY",
|
||||
"deviceKey": "BASE64_DEVICE_KEY",
|
||||
"clientKey": "BASE64_DEVICE_KEY",
|
||||
"bchLimit": 1000000
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ login TEXT NOT NULL,
|
||||
loginId INTEGER PRIMARY KEY,
|
||||
bchId INTEGER NOT NULL,
|
||||
solanaKey TEXT,
|
||||
deviceKey TEXT,
|
||||
clientKey TEXT,
|
||||
bchLimit INTEGER
|
||||
);
|
||||
|
||||
@@ -118,7 +118,7 @@ timeMs — timestamp клиента (UTC).
|
||||
|
||||
sessionPwd — строка с шага 1.
|
||||
|
||||
signatureB64 — Ed25519‐подпись preimage приватным ключом deviceKey.
|
||||
signatureB64 — Ed25519‐подпись preimage приватным ключом clientKey.
|
||||
|
||||
📤 Запрос клиента
|
||||
{
|
||||
@@ -141,7 +141,7 @@ signatureB64 — Ed25519‐подпись preimage приватным ключо
|
||||
|
||||
Восстанавливает preimage.
|
||||
|
||||
Находит deviceKey пользователя.
|
||||
Находит clientKey пользователя.
|
||||
|
||||
Проверяет Ed25519-подпись.
|
||||
|
||||
|
||||
+9
-9
@@ -3,13 +3,13 @@ package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
|
||||
* Шаг 2 (v2): создание новой сессии ТОЛЬКО через clientKey.
|
||||
*
|
||||
* Шаги:
|
||||
* 1) AuthChallenge(login) -> authNonce
|
||||
* 2) CreateAuthSession(login, sessionKey, storagePwd, timeMs, authNonce, deviceKey, signatureB64, clientInfo)
|
||||
* 2) CreateAuthSession(login, sessionKey, storagePwd, timeMs, authNonce, clientKey, signatureB64, clientInfo)
|
||||
*
|
||||
* Подпись deviceKey делается над строкой (UTF-8):
|
||||
* Подпись clientKey делается над строкой (UTF-8):
|
||||
* AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||||
*
|
||||
* Важно:
|
||||
@@ -33,9 +33,9 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
||||
private String authNonce;
|
||||
|
||||
/** Публичный ключ устройства пользователя. */
|
||||
private String deviceKey;
|
||||
private String clientKey;
|
||||
|
||||
/** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
|
||||
/** Подпись Ed25519(clientKey) над строкой AUTH_CREATE_SESSION:... (base64). */
|
||||
private String signatureB64;
|
||||
|
||||
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||
@@ -87,12 +87,12 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
||||
this.authNonce = authNonce;
|
||||
}
|
||||
|
||||
public String getDeviceKey() {
|
||||
return deviceKey;
|
||||
public String getClientKey() {
|
||||
return clientKey;
|
||||
}
|
||||
|
||||
public void setDeviceKey(String deviceKey) {
|
||||
this.deviceKey = deviceKey;
|
||||
public void setClientKey(String clientKey) {
|
||||
this.clientKey = clientKey;
|
||||
}
|
||||
|
||||
public String getSignatureB64() {
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
-180
@@ -1,180 +0,0 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Запрос GetFriendsLists — получить два списка "друзей" по connections_state.
|
||||
*
|
||||
* {
|
||||
* "op": "GetFriendsLists",
|
||||
* "requestId": "req-100",
|
||||
* "payload": {
|
||||
* "login": "anya"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Возвращает:
|
||||
* - out_friends: кому login поставил FRIEND
|
||||
* - in_friends: кто поставил FRIEND этому login
|
||||
*
|
||||
* ПРО ДОСТУП (на будущее):
|
||||
* Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей.
|
||||
*/
|
||||
public class Net_GetFriendsLists_Request extends Net_Request {
|
||||
|
||||
private String login;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ответ GetFriendsLists.
|
||||
*
|
||||
* {
|
||||
* "op": "GetFriendsLists",
|
||||
* "requestId": "req-100",
|
||||
* "status": 200,
|
||||
* "payload": {
|
||||
* "login": "Anya", // канонический регистр из БД
|
||||
* "out_friends": ["Bob", "Kate"], // кому login поставил FRIEND
|
||||
* "in_friends": ["Alex", "Kate"] // кто поставил FRIEND login
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public class Net_GetFriendsLists_Response extends Net_Response {
|
||||
|
||||
private String login;
|
||||
|
||||
private List<String> out_friends = new ArrayList<>();
|
||||
private List<String> in_friends = new ArrayList<>();
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public List<String> getOut_friends() { return out_friends; }
|
||||
public void setOut_friends(List<String> out_friends) { this.out_friends = out_friends; }
|
||||
|
||||
public List<String> getIn_friends() { return in_friends; }
|
||||
public void setIn_friends(List<String> in_friends) { this.in_friends = in_friends; }
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.connections;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.ConnectionsStateDAO;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* GetFriendsLists — получить 2 списка:
|
||||
* - out_friends: кому login поставил FRIEND
|
||||
* - in_friends: кто поставил FRIEND этому login
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - login в запросе может быть любым регистром
|
||||
* - в ответе возвращаем канонический регистр (как в solana_users.login)
|
||||
*
|
||||
* ПРИМЕЧАНИЕ:
|
||||
* Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
|
||||
*/
|
||||
public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class);
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest;
|
||||
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_FIELDS",
|
||||
"Некорректные поля: login"
|
||||
);
|
||||
}
|
||||
|
||||
final String loginAnyCase = req.getLogin().trim();
|
||||
|
||||
try {
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
|
||||
// 1) Канонизируем login через solana_users (NOCASE)
|
||||
String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
|
||||
if (canonicalLogin == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
404,
|
||||
"USER_NOT_FOUND",
|
||||
"Пользователь не найден"
|
||||
);
|
||||
}
|
||||
|
||||
int relType = (int) MsgSubType.CONNECTION_FRIEND;
|
||||
|
||||
// 2) Два списка (логины канонические)
|
||||
List<String> outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType);
|
||||
List<String> inFriends = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType);
|
||||
|
||||
Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
resp.setLogin(canonicalLogin);
|
||||
resp.setOut_friends(outFriends);
|
||||
resp.setIn_friends(inFriends);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Internal error GetFriendsLists", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.INTERNAL_ERROR,
|
||||
"INTERNAL_ERROR",
|
||||
"Внутренняя ошибка сервера"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
||||
String sql = """
|
||||
SELECT login
|
||||
FROM solana_users
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
LIMIT 1
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, loginAnyCase);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
return rs.getString("login");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
+1
-1
@@ -62,7 +62,7 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
||||
resp.setBlockchainName(u.getBlockchainName());
|
||||
resp.setSolanaKey(u.getSolanaKey());
|
||||
resp.setBlockchainKey(u.getBlockchainKey());
|
||||
resp.setDeviceKey(u.getDeviceKey());
|
||||
resp.setClientKey(u.getClientKey());
|
||||
|
||||
// Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна,
|
||||
// автоматически восстанавливаем её для существующего пользователя.
|
||||
|
||||
-240
@@ -1,240 +0,0 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Запрос AddUser — временная/тестовая регистрация локального пользователя.
|
||||
*
|
||||
* Клиент отправляет:
|
||||
*
|
||||
* {
|
||||
* "op": "AddUser",
|
||||
* "requestId": "test-add-1",
|
||||
* "payload": {
|
||||
* "login": "anya",
|
||||
* "blockchainName": "anya-001",
|
||||
* "solanaKey": "base64-ed25519-public-key-login",
|
||||
* "blockchainKey": "base64-ed25519-public-key-blockchain",
|
||||
* "deviceKey": "base64-ed25519-public-key-device",
|
||||
* "bchLimit": 1000000
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Все поля лежат внутри payload.
|
||||
*/
|
||||
public class Net_AddUser_Request extends Net_Request {
|
||||
|
||||
private String login;
|
||||
private String blockchainName;
|
||||
|
||||
/** Ключ пользователя Solana (публичный ключ логина) */
|
||||
private String solanaKey;
|
||||
|
||||
/** Ключ блокчейна (публичный ключ блокчейна) */
|
||||
private String blockchainKey;
|
||||
|
||||
/** Ключ устройства (публичный ключ устройства) */
|
||||
private String deviceKey;
|
||||
|
||||
private Integer bchLimit;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public String getBlockchainName() { return blockchainName; }
|
||||
public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
|
||||
|
||||
public String getSolanaKey() { return solanaKey; }
|
||||
public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
|
||||
|
||||
public String getBlockchainKey() { return blockchainKey; }
|
||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||
|
||||
public String getDeviceKey() { return deviceKey; }
|
||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
||||
|
||||
public Integer getBchLimit() { return bchLimit; }
|
||||
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Успешный ответ на AddUser.
|
||||
*
|
||||
* Сейчас дополнительных полей нет — достаточно status=200.
|
||||
*
|
||||
* Пример:
|
||||
* {
|
||||
* "op": "AddUser",
|
||||
* "requestId": "test-add-1",
|
||||
* "status": 200,
|
||||
* "payload": { }
|
||||
* }
|
||||
*/
|
||||
public class Net_AddUser_Response extends Net_Response {
|
||||
// При необходимости сюда можно добавить, например, флаг created/updated и т.п.
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.tempToTest;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.BlockchainStateDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import utils.blockchain.BlockchainNameUtil;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class Net_AddUser_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);
|
||||
|
||||
/** TEST ONLY */
|
||||
private static final int TEST_BCH_LIMIT = 1_000_000;
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
|
||||
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||
|| req.getBlockchainName() == null || req.getBlockchainName().isBlank()
|
||||
|| req.getSolanaKey() == null || req.getSolanaKey().isBlank()
|
||||
|| req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
|
||||
|| req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
|
||||
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_FIELDS",
|
||||
"Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
|
||||
);
|
||||
}
|
||||
|
||||
// blockchainName должен быть вида: <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 {
|
||||
byte[] blockchainKey32 = Base64.getDecoder().decode(req.getBlockchainKey());
|
||||
if (blockchainKey32.length != 32) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BLOCKCHAIN_KEY",
|
||||
"blockchainKey должен быть Base64(32 bytes)"
|
||||
);
|
||||
}
|
||||
|
||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
||||
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
c.setAutoCommit(false);
|
||||
|
||||
// 1. Проверяем, что пользователя нет
|
||||
if (usersDAO.getByLogin(req.getLogin()) != null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
409,
|
||||
"USER_ALREADY_EXISTS",
|
||||
"Пользователь с таким login уже существует"
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Проверяем, что blockchain_state ещё нет
|
||||
if (stateDAO.getByBlockchainName(req.getBlockchainName()) != null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
409,
|
||||
"BLOCKCHAIN_ALREADY_EXISTS",
|
||||
"blockchain_state уже существует"
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Создаём пользователя (solanaKey + deviceKey)
|
||||
SolanaUserEntry user = new SolanaUserEntry(
|
||||
req.getLogin(),
|
||||
req.getSolanaKey(),
|
||||
req.getDeviceKey()
|
||||
);
|
||||
|
||||
usersDAO.insert(c, user);
|
||||
|
||||
// 4. Создаём INITIAL blockchain_state (blockchainKey)
|
||||
BlockchainStateEntry st = new BlockchainStateEntry();
|
||||
st.setBlockchainName(req.getBlockchainName());
|
||||
st.setLogin(req.getLogin());
|
||||
st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
|
||||
st.setLastBlockNumber(-1);
|
||||
st.setLastBlockHash(new byte[32]);
|
||||
st.setFileSizeBytes(0);
|
||||
st.setSizeLimit(limit);
|
||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
||||
|
||||
stateDAO.upsert(c, st);
|
||||
|
||||
c.commit();
|
||||
}
|
||||
|
||||
Net_AddUser_Response resp = new Net_AddUser_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
|
||||
req.getLogin(), req.getBlockchainName(), limit);
|
||||
|
||||
return resp;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_KEY_FORMAT",
|
||||
e.getMessage()
|
||||
);
|
||||
} catch (SQLException e) {
|
||||
log.error("❌ DB error AddUser", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR",
|
||||
"Ошибка БД"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Internal error AddUser", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.INTERNAL_ERROR,
|
||||
"INTERNAL_ERROR",
|
||||
"Внутренняя ошибка сервера"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
+4
-4
@@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
* "blockchainName": "anya-001",
|
||||
* "solanaKey": "base64-ed25519-public-key-login",
|
||||
* "blockchainKey": "base64-ed25519-public-key-blockchain",
|
||||
* "deviceKey": "base64-ed25519-public-key-device",
|
||||
* "clientKey": "base64-ed25519-public-key-device",
|
||||
* "bchLimit": 1000000
|
||||
* }
|
||||
* }
|
||||
@@ -34,7 +34,7 @@ public class Net_AddUser_Request extends Net_Request {
|
||||
private String blockchainKey;
|
||||
|
||||
/** Ключ устройства (публичный ключ устройства) */
|
||||
private String deviceKey;
|
||||
private String clientKey;
|
||||
|
||||
private Integer bchLimit;
|
||||
|
||||
@@ -50,8 +50,8 @@ public class Net_AddUser_Request extends Net_Request {
|
||||
public String getBlockchainKey() { return blockchainKey; }
|
||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||
|
||||
public String getDeviceKey() { return deviceKey; }
|
||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
||||
public String getClientKey() { return clientKey; }
|
||||
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||
|
||||
public Integer getBchLimit() { return bchLimit; }
|
||||
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
|
||||
|
||||
+4
-4
@@ -26,7 +26,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
* "blockchainName": "anya-001",
|
||||
* "solanaKey": "...",
|
||||
* "blockchainKey": "...",
|
||||
* "deviceKey": "..."
|
||||
* "clientKey": "..."
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@@ -38,7 +38,7 @@ public class Net_GetUser_Response extends Net_Response {
|
||||
private String blockchainName;
|
||||
private String solanaKey;
|
||||
private String blockchainKey;
|
||||
private String deviceKey;
|
||||
private String clientKey;
|
||||
private Integer serverLastGlobalNumber;
|
||||
private String serverLastGlobalHash;
|
||||
private Long serverBlockchainSizeBytes;
|
||||
@@ -59,8 +59,8 @@ public class Net_GetUser_Response extends Net_Response {
|
||||
public String getBlockchainKey() { return blockchainKey; }
|
||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||
|
||||
public String getDeviceKey() { return deviceKey; }
|
||||
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
||||
public String getClientKey() { return clientKey; }
|
||||
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||
|
||||
public Integer getServerLastGlobalNumber() { return serverLastGlobalNumber; }
|
||||
public void setServerLastGlobalNumber(Integer serverLastGlobalNumber) { this.serverLastGlobalNumber = serverLastGlobalNumber; }
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ public class Net_GetUserParam_Handler implements JsonMessageHandler {
|
||||
resp.setParam(e.getParam());
|
||||
resp.setTime_ms(e.getTimeMs());
|
||||
resp.setValue(e.getValue());
|
||||
resp.setDevice_key(e.getDeviceKey());
|
||||
resp.setClient_key(e.getClientKey());
|
||||
resp.setSignature(e.getSignature());
|
||||
|
||||
return resp;
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ public class Net_ListUserParams_Handler implements JsonMessageHandler {
|
||||
it.setParam(e.getParam());
|
||||
it.setTime_ms(e.getTimeMs());
|
||||
it.setValue(e.getValue());
|
||||
it.setDevice_key(e.getDeviceKey());
|
||||
it.setClient_key(e.getClientKey());
|
||||
it.setSignature(e.getSignature());
|
||||
items.add(it);
|
||||
}
|
||||
|
||||
+14
-14
@@ -28,8 +28,8 @@ import java.sql.SQLException;
|
||||
*
|
||||
* Делает (MVP, без "сессий"):
|
||||
* 1) Проверка входных полей.
|
||||
* 2) Проверка подписи Ed25519 по device_key.
|
||||
* 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
|
||||
* 2) Проверка подписи Ed25519 по client_key.
|
||||
* 3) Проверка, что пользователь существует и что client_key принадлежит этому login.
|
||||
* 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
|
||||
*
|
||||
* ВАЖНО:
|
||||
@@ -50,14 +50,14 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
||||
|| req.getParam() == null || req.getParam().isBlank()
|
||||
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|
||||
|| req.getValue() == null
|
||||
|| req.getDevice_key() == null || req.getDevice_key().isBlank()
|
||||
|| req.getClient_key() == null || req.getClient_key().isBlank()
|
||||
|| req.getSignature() == null || req.getSignature().isBlank()) {
|
||||
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_FIELDS",
|
||||
"Некорректные поля: login/param/time_ms/value/device_key/signature"
|
||||
"Некорректные поля: login/param/time_ms/value/client_key/signature"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
||||
final String param = req.getParam().trim();
|
||||
final long timeMs = req.getTime_ms();
|
||||
final String value = req.getValue();
|
||||
final String deviceKeyB64 = req.getDevice_key().trim();
|
||||
final String clientKeyB64 = req.getClient_key().trim();
|
||||
final String signatureB64 = req.getSignature().trim();
|
||||
|
||||
try {
|
||||
@@ -73,14 +73,14 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
||||
byte[] pubKey32;
|
||||
byte[] sig64;
|
||||
try {
|
||||
pubKey32 = Base64Ws.decodeLen(deviceKeyB64, 32, "device_key");
|
||||
pubKey32 = Base64Ws.decodeLen(clientKeyB64, 32, "client_key");
|
||||
sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BASE64",
|
||||
"device_key/signature должны быть Base64"
|
||||
"client_key/signature должны быть Base64"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,23 +120,23 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// 2) device key must match the user's stored deviceKey
|
||||
String userDeviceKey = user.getDeviceKey();
|
||||
if (userDeviceKey == null || userDeviceKey.isBlank()) {
|
||||
// 2) client key must match the user's stored clientKey
|
||||
String userClientKey = user.getClientKey();
|
||||
if (userClientKey == null || userClientKey.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"USER_DEVICE_KEY_EMPTY",
|
||||
"У пользователя не задан deviceKey в БД"
|
||||
"У пользователя не задан clientKey в БД"
|
||||
);
|
||||
}
|
||||
|
||||
if (!userDeviceKey.trim().equals(deviceKeyB64)) {
|
||||
if (!userClientKey.trim().equals(clientKeyB64)) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
403,
|
||||
"DEVICE_KEY_MISMATCH",
|
||||
"device_key не соответствует пользователю"
|
||||
"client_key не соответствует пользователю"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
||||
param,
|
||||
timeMs,
|
||||
value,
|
||||
deviceKeyB64,
|
||||
clientKeyB64,
|
||||
signatureB64
|
||||
);
|
||||
|
||||
|
||||
-640
@@ -1,640 +0,0 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Запрос GetUserParam — получить один параметр пользователя.
|
||||
*
|
||||
* {
|
||||
* "op": "GetUserParam",
|
||||
* "requestId": "req-1",
|
||||
* "payload": {
|
||||
* "login": "anya",
|
||||
* "param": "feed:lastSeenGlobal"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ПРО ДОСТУП (на будущее):
|
||||
* ---------------------------------------------------------------------------------
|
||||
* Сейчас (MVP) этот запрос не ограничивает просмотр параметров, т.к. проект в тестовом режиме.
|
||||
* Позже, вероятно, потребуется ограничить: кто и какие параметры может читать (сессия/права).
|
||||
* Но для MVP эти проверки не нужны.
|
||||
* ---------------------------------------------------------------------------------
|
||||
*/
|
||||
public class Net_GetUserParam_Request extends Net_Request {
|
||||
|
||||
private String login;
|
||||
private String param;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public String getParam() { return param; }
|
||||
public void setParam(String param) { this.param = param; }
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Ответ GetUserParam.
|
||||
*
|
||||
* Если найден:
|
||||
* {
|
||||
* "op": "GetUserParam",
|
||||
* "requestId": "req-1",
|
||||
* "status": 200,
|
||||
* "payload": {
|
||||
* "login": "anya",
|
||||
* "param": "feed:lastSeenGlobal",
|
||||
* "time_ms": 1736000000123,
|
||||
* "value": "105",
|
||||
* "device_key": "base64-32",
|
||||
* "signature": "base64-64"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Если не найден:
|
||||
* status=404, payload пустой.
|
||||
*/
|
||||
public class Net_GetUserParam_Response extends Net_Response {
|
||||
|
||||
private String login;
|
||||
private String param;
|
||||
private Long time_ms;
|
||||
private String value;
|
||||
private String device_key;
|
||||
private String signature;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public String getParam() { return param; }
|
||||
public void setParam(String param) { this.param = param; }
|
||||
|
||||
public Long getTime_ms() { return time_ms; }
|
||||
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
|
||||
public String getDevice_key() { return device_key; }
|
||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
||||
|
||||
public String getSignature() { return signature; }
|
||||
public void setSignature(String signature) { this.signature = signature; }
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Запрос ListUserParams — получить все сохранённые параметры пользователя.
|
||||
*
|
||||
* {
|
||||
* "op": "ListUserParams",
|
||||
* "requestId": "req-2",
|
||||
* "payload": {
|
||||
* "login": "anya"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ПРО ДОСТУП (на будущее):
|
||||
* ---------------------------------------------------------------------------------
|
||||
* Сейчас (MVP) запрос не ограничивает просмотр параметров.
|
||||
* В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
|
||||
* Для MVP эти проверки не нужны.
|
||||
* ---------------------------------------------------------------------------------
|
||||
*/
|
||||
public class Net_ListUserParams_Request extends Net_Request {
|
||||
|
||||
private String login;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ответ ListUserParams — список всех параметров пользователя.
|
||||
*
|
||||
* {
|
||||
* "op": "ListUserParams",
|
||||
* "requestId": "req-2",
|
||||
* "status": 200,
|
||||
* "payload": {
|
||||
* "login": "anya",
|
||||
* "params": [
|
||||
* {
|
||||
* "login": "anya",
|
||||
* "param": "feed:lastSeenGlobal",
|
||||
* "time_ms": 1736000000123,
|
||||
* "value": "105",
|
||||
* "device_key": "base64-32",
|
||||
* "signature": "base64-64"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public class Net_ListUserParams_Response extends Net_Response {
|
||||
|
||||
private String login;
|
||||
private List<Item> params = new ArrayList<>();
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public List<Item> getParams() { return params; }
|
||||
public void setParams(List<Item> params) { this.params = params; }
|
||||
|
||||
public static class Item {
|
||||
private String login;
|
||||
private String param;
|
||||
private Long time_ms;
|
||||
private String value;
|
||||
private String device_key;
|
||||
private String signature;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public String getParam() { return param; }
|
||||
public void setParam(String param) { this.param = param; }
|
||||
|
||||
public Long getTime_ms() { return time_ms; }
|
||||
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
|
||||
public String getDevice_key() { return device_key; }
|
||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
||||
|
||||
public String getSignature() { return signature; }
|
||||
public void setSignature(String signature) { this.signature = signature; }
|
||||
}
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
|
||||
*
|
||||
* Клиент отправляет:
|
||||
*
|
||||
* {
|
||||
* "op": "UpsertUserParam",
|
||||
* "requestId": "req-123",
|
||||
* "payload": {
|
||||
* "login": "anya",
|
||||
* "param": "feed:lastSeenGlobal",
|
||||
* "time_ms": 1736000000123,
|
||||
* "value": "105",
|
||||
* "device_key": "base64-ed25519-public-key-32",
|
||||
* "signature": "base64-ed25519-signature-64"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Подпись считается от UTF-8 строки:
|
||||
* USER_PARAMETER_PREFIX + login + param + time_ms + value
|
||||
*/
|
||||
public class Net_UpsertUserParam_Request extends Net_Request {
|
||||
|
||||
private String login;
|
||||
private String param;
|
||||
private Long time_ms;
|
||||
private String value;
|
||||
|
||||
private String device_key;
|
||||
private String signature;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public String getParam() { return param; }
|
||||
public void setParam(String param) { this.param = param; }
|
||||
|
||||
public Long getTime_ms() { return time_ms; }
|
||||
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
|
||||
public String getDevice_key() { return device_key; }
|
||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
||||
|
||||
public String getSignature() { return signature; }
|
||||
public void setSignature(String signature) { this.signature = signature; }
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Ответ на UpsertUserParam.
|
||||
*
|
||||
* Успех:
|
||||
* {
|
||||
* "op": "UpsertUserParam",
|
||||
* "requestId": "req-123",
|
||||
* "status": 200,
|
||||
* "payload": { }
|
||||
* }
|
||||
*/
|
||||
public class Net_UpsertUserParam_Response extends Net_Response {
|
||||
// MVP: без payload. При желании позже можно добавить created/updated.
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.UserParamsDAO;
|
||||
import shine.db.entities.UserParamEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
|
||||
/**
|
||||
* GetUserParam — получить один параметр пользователя.
|
||||
*
|
||||
* ПРО ДОСТУП (на будущее):
|
||||
* ---------------------------------------------------------------------------------
|
||||
* Сейчас (MVP) запрос не ограничивает просмотр параметров.
|
||||
* В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
|
||||
* Для MVP эти проверки не нужны.
|
||||
* ---------------------------------------------------------------------------------
|
||||
*/
|
||||
public class Net_GetUserParam_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_GetUserParam_Handler.class);
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_GetUserParam_Request req = (Net_GetUserParam_Request) baseRequest;
|
||||
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||
|| req.getParam() == null || req.getParam().isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_FIELDS",
|
||||
"Некорректные поля: login/param"
|
||||
);
|
||||
}
|
||||
|
||||
String login = req.getLogin().trim();
|
||||
String param = req.getParam().trim();
|
||||
|
||||
try {
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
UserParamsDAO dao = UserParamsDAO.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
UserParamEntry e = dao.getByLoginAndParam(c, login, param);
|
||||
|
||||
if (e == null) {
|
||||
Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(404);
|
||||
return resp;
|
||||
}
|
||||
|
||||
Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
resp.setLogin(e.getLogin());
|
||||
resp.setParam(e.getParam());
|
||||
resp.setTime_ms(e.getTimeMs());
|
||||
resp.setValue(e.getValue());
|
||||
resp.setDevice_key(e.getDeviceKey());
|
||||
resp.setSignature(e.getSignature());
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Internal error GetUserParam", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.INTERNAL_ERROR,
|
||||
"INTERNAL_ERROR",
|
||||
"Внутренняя ошибка сервера"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.UserParamsDAO;
|
||||
import shine.db.entities.UserParamEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ListUserParams — получить все параметры пользователя.
|
||||
*
|
||||
* ПРО ДОСТУП (на будущее):
|
||||
* ---------------------------------------------------------------------------------
|
||||
* Сейчас (MVP) запрос не ограничивает просмотр параметров.
|
||||
* В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
|
||||
* Для MVP эти проверки не нужны.
|
||||
* ---------------------------------------------------------------------------------
|
||||
*/
|
||||
public class Net_ListUserParams_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_ListUserParams_Handler.class);
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_ListUserParams_Request req = (Net_ListUserParams_Request) baseRequest;
|
||||
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_FIELDS",
|
||||
"Некорректные поля: login"
|
||||
);
|
||||
}
|
||||
|
||||
String login = req.getLogin().trim();
|
||||
|
||||
try {
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
UserParamsDAO dao = UserParamsDAO.getInstance();
|
||||
|
||||
List<UserParamEntry> entries;
|
||||
try (Connection c = db.getConnection()) {
|
||||
entries = dao.getByLogin(c, login);
|
||||
}
|
||||
|
||||
Net_ListUserParams_Response resp = new Net_ListUserParams_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
resp.setLogin(login);
|
||||
|
||||
List<Net_ListUserParams_Response.Item> items = new ArrayList<>();
|
||||
for (UserParamEntry e : entries) {
|
||||
Net_ListUserParams_Response.Item it = new Net_ListUserParams_Response.Item();
|
||||
it.setLogin(e.getLogin());
|
||||
it.setParam(e.getParam());
|
||||
it.setTime_ms(e.getTimeMs());
|
||||
it.setValue(e.getValue());
|
||||
it.setDevice_key(e.getDeviceKey());
|
||||
it.setSignature(e.getSignature());
|
||||
items.add(it);
|
||||
}
|
||||
resp.setParams(items);
|
||||
|
||||
return resp;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Internal error ListUserParams", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.INTERNAL_ERROR,
|
||||
"INTERNAL_ERROR",
|
||||
"Внутренняя ошибка сервера"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
package server.logic.ws_protocol.JSON.handlers.userParams;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.UserParamsDAO;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.UserParamEntry;
|
||||
import utils.config.ShineSignatureConstants;
|
||||
import utils.crypto.Ed25519Util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Net_UpsertUserParam_Handler
|
||||
*
|
||||
* Делает (MVP, без "сессий"):
|
||||
* 1) Проверка входных полей.
|
||||
* 2) Проверка подписи Ed25519 по device_key.
|
||||
* 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
|
||||
* 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - НИКАКИХ ручных транзакций / BEGIN здесь нет.
|
||||
* - autoCommit=true, каждый statement завершённый сам по себе.
|
||||
* - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
|
||||
* наш финальный UPSERT просто вернёт 0 обновлённых строк.
|
||||
*/
|
||||
public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;
|
||||
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||
|| req.getParam() == null || req.getParam().isBlank()
|
||||
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|
||||
|| req.getValue() == null
|
||||
|| req.getDevice_key() == null || req.getDevice_key().isBlank()
|
||||
|| req.getSignature() == null || req.getSignature().isBlank()) {
|
||||
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_FIELDS",
|
||||
"Некорректные поля: login/param/time_ms/value/device_key/signature"
|
||||
);
|
||||
}
|
||||
|
||||
final String login = req.getLogin().trim();
|
||||
final String param = req.getParam().trim();
|
||||
final long timeMs = req.getTime_ms();
|
||||
final String value = req.getValue();
|
||||
final String deviceKeyB64 = req.getDevice_key().trim();
|
||||
final String signatureB64 = req.getSignature().trim();
|
||||
|
||||
try {
|
||||
// ---------------- Base64 decode ----------------
|
||||
byte[] pubKey32;
|
||||
byte[] sig64;
|
||||
try {
|
||||
pubKey32 = Base64.getDecoder().decode(deviceKeyB64);
|
||||
sig64 = Base64.getDecoder().decode(signatureB64);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_BASE64",
|
||||
"device_key/signature должны быть Base64"
|
||||
);
|
||||
}
|
||||
|
||||
if (pubKey32.length != 32) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_DEVICE_KEY",
|
||||
"device_key должен быть Base64(32 bytes)"
|
||||
);
|
||||
}
|
||||
if (sig64.length != 64) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.BAD_REQUEST,
|
||||
"BAD_SIGNATURE",
|
||||
"signature должна быть Base64(64 bytes)"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- Signature verify ----------------
|
||||
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
|
||||
+ login
|
||||
+ param
|
||||
+ timeMs
|
||||
+ value;
|
||||
|
||||
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
|
||||
if (!sigOk) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
403,
|
||||
"SIGNATURE_INVALID",
|
||||
"Подпись не прошла проверку"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- DB checks + upsert ----------------
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
||||
UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
// 1) user exists
|
||||
SolanaUserEntry user = usersDAO.getByLogin(c, login);
|
||||
if (user == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
404,
|
||||
"USER_NOT_FOUND",
|
||||
"Пользователь не найден"
|
||||
);
|
||||
}
|
||||
|
||||
// 2) device key must match the user's stored deviceKey
|
||||
String userDeviceKey = user.getDeviceKey();
|
||||
if (userDeviceKey == null || userDeviceKey.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"USER_DEVICE_KEY_EMPTY",
|
||||
"У пользователя не задан deviceKey в БД"
|
||||
);
|
||||
}
|
||||
|
||||
if (!userDeviceKey.trim().equals(deviceKeyB64)) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
403,
|
||||
"DEVICE_KEY_MISMATCH",
|
||||
"device_key не соответствует пользователю"
|
||||
);
|
||||
}
|
||||
|
||||
// 3) atomic upsert-if-newer
|
||||
UserParamEntry e = new UserParamEntry(
|
||||
login,
|
||||
param,
|
||||
timeMs,
|
||||
value,
|
||||
deviceKeyB64,
|
||||
signatureB64
|
||||
);
|
||||
|
||||
int changed = paramsDAO.upsertIfNewer(c, e);
|
||||
|
||||
Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
if (changed == 1) {
|
||||
log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
|
||||
} else {
|
||||
// 0 строк — значит в БД уже есть time_ms >= incoming
|
||||
log.info("ℹ️ UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
} catch (SQLException e) {
|
||||
log.error("❌ DB error UpsertUserParam", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"DB_ERROR",
|
||||
"Ошибка БД"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Internal error UpsertUserParam", e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.INTERNAL_ERROR,
|
||||
"INTERNAL_ERROR",
|
||||
"Внутренняя ошибка сервера"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
+4
-4
@@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
* "param": "feed:lastSeenGlobal",
|
||||
* "time_ms": 1736000000123,
|
||||
* "value": "105",
|
||||
* "device_key": "base64-32",
|
||||
* "client_key": "base64-32",
|
||||
* "signature": "base64-64"
|
||||
* }
|
||||
* }
|
||||
@@ -29,7 +29,7 @@ public class Net_GetUserParam_Response extends Net_Response {
|
||||
private String param;
|
||||
private Long time_ms;
|
||||
private String value;
|
||||
private String device_key;
|
||||
private String client_key;
|
||||
private String signature;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
@@ -44,8 +44,8 @@ public class Net_GetUserParam_Response extends Net_Response {
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
|
||||
public String getDevice_key() { return device_key; }
|
||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
||||
public String getClient_key() { return client_key; }
|
||||
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||
|
||||
public String getSignature() { return signature; }
|
||||
public void setSignature(String signature) { this.signature = signature; }
|
||||
|
||||
+4
-4
@@ -20,7 +20,7 @@ import java.util.List;
|
||||
* "param": "feed:lastSeenGlobal",
|
||||
* "time_ms": 1736000000123,
|
||||
* "value": "105",
|
||||
* "device_key": "base64-32",
|
||||
* "client_key": "base64-32",
|
||||
* "signature": "base64-64"
|
||||
* },
|
||||
* ...
|
||||
@@ -44,7 +44,7 @@ public class Net_ListUserParams_Response extends Net_Response {
|
||||
private String param;
|
||||
private Long time_ms;
|
||||
private String value;
|
||||
private String device_key;
|
||||
private String client_key;
|
||||
private String signature;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
@@ -59,8 +59,8 @@ public class Net_ListUserParams_Response extends Net_Response {
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
|
||||
public String getDevice_key() { return device_key; }
|
||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
||||
public String getClient_key() { return client_key; }
|
||||
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||
|
||||
public String getSignature() { return signature; }
|
||||
public void setSignature(String signature) { this.signature = signature; }
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
* "param": "feed:lastSeenGlobal",
|
||||
* "time_ms": 1736000000123,
|
||||
* "value": "105",
|
||||
* "device_key": "base64-ed25519-public-key-32",
|
||||
* "client_key": "base64-ed25519-public-key-32",
|
||||
* "signature": "base64-ed25519-signature-64"
|
||||
* }
|
||||
* }
|
||||
@@ -30,7 +30,7 @@ public class Net_UpsertUserParam_Request extends Net_Request {
|
||||
private Long time_ms;
|
||||
private String value;
|
||||
|
||||
private String device_key;
|
||||
private String client_key;
|
||||
private String signature;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
@@ -45,8 +45,8 @@ public class Net_UpsertUserParam_Request extends Net_Request {
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
|
||||
public String getDevice_key() { return device_key; }
|
||||
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
||||
public String getClient_key() { return client_key; }
|
||||
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||
|
||||
public String getSignature() { return signature; }
|
||||
public void setSignature(String signature) { this.signature = signature; }
|
||||
|
||||
+2
-2
@@ -62,9 +62,9 @@ public class Net_SendDirectMessage_Handler implements JsonMessageHandler {
|
||||
|
||||
byte[] publicKey32;
|
||||
try {
|
||||
publicKey32 = Ed25519Util.keyFromBase64(fromUser.getDeviceKey());
|
||||
publicKey32 = Ed25519Util.keyFromBase64(fromUser.getClientKey());
|
||||
} catch (Exception e) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный deviceKey отправителя");
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный clientKey отправителя");
|
||||
}
|
||||
if (!Ed25519Util.verify(packet.signedBody, packet.signature64, publicKey32)) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку");
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ final class SignedMessagesCore {
|
||||
if (from == null || to == null) {
|
||||
throw new IllegalArgumentException("USER_NOT_FOUND");
|
||||
}
|
||||
byte[] pubKey32 = Ed25519Util.keyFromBase64(from.getDeviceKey());
|
||||
byte[] pubKey32 = Ed25519Util.keyFromBase64(from.getClientKey());
|
||||
if (!Ed25519Util.verify(block.signedBody, block.signature64, pubKey32)) {
|
||||
throw new IllegalArgumentException("BAD_SIGNATURE");
|
||||
}
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Проверка подписи CreateAuthSession(v2) по deviceKey пользователя.
|
||||
// * Проверка подписи CreateAuthSession(v2) по clientKey пользователя.
|
||||
// * Подпись проверяется над preimageCreateAuthSession(...).
|
||||
// */
|
||||
// public static boolean verifyCreateAuthSessionSignature(
|
||||
@@ -42,8 +42,8 @@
|
||||
// String signatureB64
|
||||
// ) throws IllegalArgumentException {
|
||||
//
|
||||
// // user.getDeviceKey() — base64 публичного ключа (32 байта)
|
||||
// byte[] publicKey32 = decodeBase64Any(user.getDeviceKey());
|
||||
// // user.getClientKey() — base64 публичного ключа (32 байта)
|
||||
// byte[] publicKey32 = decodeBase64Any(user.getClientKey());
|
||||
// byte[] signature64 = decodeBase64Any(signatureB64);
|
||||
//
|
||||
// byte[] preimage = preimageCreateAuthSession(login, timeMs, authNonce);
|
||||
|
||||
@@ -1,552 +0,0 @@
|
||||
package server.logic;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.binary.handlers.*;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Обработчик входящих сообщение на сервер.
|
||||
* По коду сообщения (первые 4 байта сообщения) находи нужный хэндлер и передаёт в него сообщение
|
||||
* Получает и возвращает ответ от хэндлера
|
||||
*/
|
||||
public final class InboundMessageProcessor {
|
||||
private static final Logger log = LoggerFactory.getLogger(InboundMessageProcessor.class);
|
||||
|
||||
private static final Map<Integer, MessageHandler> HANDLERS = Map.of(
|
||||
// WireCodes.Op.PING, new PingHandler()
|
||||
// WireCodes.Op.ADD_BLOCK, new AddBlockHandler(),
|
||||
// WireCodes.Op.GET_BLOCKCHAIN,new GetBlockchainHandler()
|
||||
// WireCodes.Op.SEARCH_USERS, new SearchUsersHandler(),
|
||||
// WireCodes.Op.GET_LAST_BLOCK_INFO,new GetLastBlockInfoHandler()
|
||||
|
||||
);
|
||||
|
||||
private InboundMessageProcessor() {}
|
||||
|
||||
public static byte[] process(byte[] msg) {
|
||||
if (msg == null || msg.length < 4)
|
||||
return intTo4Bytes(WireCodes.Status.BAD_REQUEST);
|
||||
|
||||
int op = first4ToInt(msg);
|
||||
MessageHandler h = HANDLERS.get(op);
|
||||
if (h == null) {
|
||||
log.warn("Неизвестная операция: {}", op);
|
||||
return intTo4Bytes(WireCodes.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
return h.handle(msg);
|
||||
} catch (Exception e) {
|
||||
log.error("Ошибка при обработке операции {}", op, e);
|
||||
return intTo4Bytes(WireCodes.Status.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private static int first4ToInt(byte[] msg) {
|
||||
return ByteBuffer.wrap(msg, 0, 4)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.getInt();
|
||||
}
|
||||
|
||||
public static byte[] intTo4Bytes(int code) {
|
||||
return ByteBuffer.allocate(4)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putInt(code)
|
||||
.array();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
package server.logic.ws_protocol.binary.handlers;
|
||||
|
||||
/**
|
||||
* Общий интерфейс для всех обработчиков входящих сообщений.
|
||||
*/
|
||||
public interface MessageHandler {
|
||||
/**
|
||||
* Обработать входящее сообщение и вернуть бинарный ответ.
|
||||
*/
|
||||
byte[] handle(byte[] msg);
|
||||
}
|
||||
|
||||
package server.ws;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import shine.db.dao.BlockchainStateDAO;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
import utils.files.FileStoreUtil;
|
||||
import shine.log.BlockchainAdminNotifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ===============================================================
|
||||
* BlockchainTmpRecoveryOnStartup — восстановление консистентности
|
||||
* blockchain файлов при старте сервера.
|
||||
*
|
||||
* Сценарий проблемы:
|
||||
* - при добавлении блока сначала пишется <name>.tmp_bch
|
||||
* - потом коммитится БД (state.fileSizeBytes)
|
||||
* - потом tmp переименовывается поверх <name>.bch (атомарно, если возможно)
|
||||
*
|
||||
* Если сервер упал в середине, может остаться tmp:
|
||||
* - tmp есть, а основной .bch остался старым
|
||||
* - tmp есть, а основной .bch уже удалили/заменить не успели
|
||||
* - tmp есть, а БД успела/не успела обновиться
|
||||
*
|
||||
* Этот класс при старте:
|
||||
* - ищет все *.tmp_bch в data/
|
||||
* - сравнивает размеры:
|
||||
* - tmp
|
||||
* - main (если есть)
|
||||
* - state.fileSizeBytes (если есть)
|
||||
*
|
||||
* Правила:
|
||||
*
|
||||
* A) state есть:
|
||||
* - если stateSize == mainSize => tmp удаляем
|
||||
* - если stateSize == tmpSize => tmp ставим на место main (atomicReplaceBlockchainFile)
|
||||
* - иначе => КРИТИЧЕСКАЯ ОШИБКА: сервер останавливаем + уведомление администратору
|
||||
*
|
||||
* B) state НЕТ:
|
||||
* - если main НЕТ и tmp ЕСТЬ => tmp удаляем (мусор после падения/неуспешной транзакции)
|
||||
* - если main ЕСТЬ и tmp ЕСТЬ => КРИТИЧЕСКАЯ ОШИБКА: уведомление администратору + стоп сервера
|
||||
*
|
||||
* Логирование:
|
||||
* - обо всех восстановленных/удалённых tmp пишем в лог
|
||||
* - если tmp-файлов нет — тоже пишем в лог
|
||||
* ===============================================================
|
||||
*/
|
||||
public final class BlockchainTmpRecoveryOnStartup {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BlockchainTmpRecoveryOnStartup.class);
|
||||
|
||||
private BlockchainTmpRecoveryOnStartup() {}
|
||||
|
||||
/**
|
||||
* Запуск восстановления.
|
||||
* Если обнаружена ситуация, когда размеры не совпали и сервер сам не может чинить — бросаем исключение.
|
||||
*/
|
||||
public static void runRecoveryOrThrow() {
|
||||
FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||
|
||||
Path dataDir = Paths.get(FileStoreUtil.DATA_DIR_NAME);
|
||||
ensureDirExists(dataDir);
|
||||
|
||||
List<Path> tmpFiles = listTmpFiles(dataDir);
|
||||
|
||||
if (tmpFiles.isEmpty()) {
|
||||
log.info("🟢 BlockchainTmpRecovery: временных *.tmp_bch файлов не найдено — восстановление не требуется.");
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn("🟡 BlockchainTmpRecovery: найдено временных файлов: {}", tmpFiles.size());
|
||||
|
||||
for (Path tmpPath : tmpFiles) {
|
||||
String fileName = tmpPath.getFileName().toString();
|
||||
String blockchainName = extractBlockchainNameFromTmp(fileName);
|
||||
|
||||
if (blockchainName == null || blockchainName.isBlank()) {
|
||||
// странное имя — не трогаем автоматически, но это уже повод дернуть админа
|
||||
BlockchainAdminNotifier.critical(
|
||||
"НАЙДЕН TMP-ФАЙЛ С НЕОЖИДАННЫМ ИМЕНЕМ: " + fileName + " (не могу определить blockchainName).",
|
||||
null
|
||||
);
|
||||
throw new IllegalStateException("Bad tmp file name: " + fileName);
|
||||
}
|
||||
|
||||
Path mainPath = dataDir.resolve(fs.buildBlockchainFileName(blockchainName));
|
||||
|
||||
long tmpSize = safeSize(tmpPath);
|
||||
boolean mainExists = Files.exists(mainPath);
|
||||
long mainSize = mainExists ? safeSize(mainPath) : -1L;
|
||||
|
||||
BlockchainStateEntry st = null;
|
||||
try {
|
||||
st = stateDAO.getByBlockchainName(blockchainName);
|
||||
} catch (SQLException e) {
|
||||
BlockchainAdminNotifier.critical(
|
||||
"ОШИБКА БД ПРИ ВОССТАНОВЛЕНИИ TMP: blockchainName=" + blockchainName + " (сервер остановлен).",
|
||||
e
|
||||
);
|
||||
throw new IllegalStateException("DB error during tmp recovery for " + blockchainName, e);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CASE B) state НЕТ
|
||||
// ============================================================
|
||||
if (st == null) {
|
||||
|
||||
if (!mainExists) {
|
||||
// НЕТ state, НЕТ main, есть tmp => удаляем tmp
|
||||
log.warn("🟠 BlockchainTmpRecovery: state отсутствует и main отсутствует, но tmp найден => удаляем tmp. blockchainName={}, tmpSize={}",
|
||||
blockchainName, tmpSize);
|
||||
safeDelete(tmpPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// НЕТ state, но main есть и tmp есть => это уже подозрительно
|
||||
BlockchainAdminNotifier.critical(
|
||||
"НЕСОГЛАСОВАННОСТЬ: ЕСТЬ main И tmp, НО НЕТ state В БД. " +
|
||||
"blockchainName=" + blockchainName +
|
||||
", mainSize=" + mainSize +
|
||||
", tmpSize=" + tmpSize +
|
||||
". СЕРВЕР ОСТАНОВЛЕН. " +
|
||||
"ПОДОЗРЕНИЕ: файлы могли быть изменены вне сервера.",
|
||||
null
|
||||
);
|
||||
throw new IllegalStateException("State missing but both main and tmp exist for " + blockchainName);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CASE A) state ЕСТЬ
|
||||
// ============================================================
|
||||
long stateSize = st.getFileSizeBytes();
|
||||
|
||||
// 1) stateSize == mainSize => tmp мусор
|
||||
if (mainExists && mainSize == stateSize) {
|
||||
log.info("🟢 BlockchainTmpRecovery: stateSize совпадает с main => tmp удаляем. blockchainName={}, stateSize={}, mainSize={}, tmpSize={}",
|
||||
blockchainName, stateSize, mainSize, tmpSize);
|
||||
safeDelete(tmpPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2) stateSize == tmpSize => tmp это актуальная версия, ставим на место main
|
||||
if (tmpSize == stateSize) {
|
||||
log.warn("🟡 BlockchainTmpRecovery: stateSize совпадает с tmp => восстанавливаем main из tmp. blockchainName={}, stateSize={}, mainSize={}, tmpSize={}",
|
||||
blockchainName, stateSize, mainSize, tmpSize);
|
||||
|
||||
try {
|
||||
// метод уже есть и делает move tmp->main с попыткой ATOMIC_MOVE
|
||||
fs.atomicReplaceBlockchainFile(blockchainName);
|
||||
|
||||
// после move tmp должен исчезнуть сам (перемещён)
|
||||
log.info("✅ BlockchainTmpRecovery: восстановление выполнено. blockchainName={}, newMainSize={}",
|
||||
blockchainName, safeSize(mainPath));
|
||||
|
||||
} catch (Exception e) {
|
||||
BlockchainAdminNotifier.critical(
|
||||
"НЕ УДАЛОСЬ ВОССТАНОВИТЬ main ИЗ tmp (move failed). " +
|
||||
"blockchainName=" + blockchainName +
|
||||
", stateSize=" + stateSize +
|
||||
", mainSize=" + mainSize +
|
||||
", tmpSize=" + tmpSize +
|
||||
". СЕРВЕР ОСТАНОВЛЕН.",
|
||||
e
|
||||
);
|
||||
throw new IllegalStateException("Cannot replace main from tmp for " + blockchainName, e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3) НИЧЕГО НЕ СОВПАЛО => критическая ситуация
|
||||
BlockchainAdminNotifier.critical(
|
||||
"ФАТАЛЬНАЯ НЕСОГЛАСОВАННОСТЬ BLOCKCHAIN ФАЙЛОВ. " +
|
||||
"blockchainName=" + blockchainName +
|
||||
", stateSize=" + stateSize +
|
||||
", mainExists=" + mainExists +
|
||||
", mainSize=" + mainSize +
|
||||
", tmpSize=" + tmpSize +
|
||||
". СЕРВЕР ОСТАНОВЛЕН. " +
|
||||
"ТУТ НУЖНО УВЕДОМЛЕНИЕ АДМИНИСТРАТОРУ: возможно файлы изменены вручную/другой программой.",
|
||||
null
|
||||
);
|
||||
throw new IllegalStateException("Blockchain files mismatch for " + blockchainName);
|
||||
}
|
||||
|
||||
log.info("✅ BlockchainTmpRecovery: обработка tmp-файлов завершена.");
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* =============================== Helpers ============================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
private static void ensureDirExists(Path dir) {
|
||||
try {
|
||||
if (!Files.exists(dir)) {
|
||||
Files.createDirectories(dir);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot create data dir: " + dir, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Path> listTmpFiles(Path dataDir) {
|
||||
List<Path> out = new ArrayList<>();
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dataDir, "*" + FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION)) {
|
||||
for (Path p : ds) {
|
||||
if (Files.isRegularFile(p)) out.add(p);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot list tmp files in: " + dataDir, e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Из "anya0001.tmp_bch" -> "anya0001"
|
||||
*/
|
||||
private static String extractBlockchainNameFromTmp(String tmpFileName) {
|
||||
if (tmpFileName == null) return null;
|
||||
if (!tmpFileName.endsWith(FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION)) return null;
|
||||
|
||||
String base = tmpFileName.substring(0, tmpFileName.length() - FileStoreUtil.BLOCKCHAIN_TMP_EXTENSION.length());
|
||||
|
||||
// базовая защита: не допускаем слэши/.. даже если кто-то подложил файл
|
||||
if (base.isBlank()) return null;
|
||||
if (base.contains("/") || base.contains("\\") || base.contains("..")) return null;
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
private static long safeSize(Path p) {
|
||||
try {
|
||||
return Files.size(p);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot read file size: " + p, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void safeDelete(Path p) {
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot delete file: " + p, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
package server.ws;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import org.eclipse.jetty.websocket.api.annotations.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.InboundMessageProcessor;
|
||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.JsonInboundProcessor;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@WebSocket
|
||||
public class BlockchainWsEndpoint {
|
||||
private static final Logger log = LoggerFactory.getLogger(BlockchainWsEndpoint.class);
|
||||
|
||||
private Session session;
|
||||
|
||||
/** Контекст для текущего WebSocket-соединения. */
|
||||
private final ConnectionContext connectionContext = new ConnectionContext();
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(Session session) {
|
||||
this.session = session;
|
||||
// Привязываем WebSocket-сессию к ConnectionContext
|
||||
connectionContext.setWsSession(session);
|
||||
log.info("WS connected: {}", session.getRemoteAddress());
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onBinary(byte[] payload, int offset, int length) {
|
||||
byte[] msg = new byte[length];
|
||||
System.arraycopy(payload, offset, msg, 0, length);
|
||||
|
||||
// Асинхронно обрабатываем входящее бинарное сообщение
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> InboundMessageProcessor.process(msg))
|
||||
.thenAccept(resp -> {
|
||||
if (resp != null && session != null && session.isOpen()) {
|
||||
session.getRemote().sendBytes(ByteBuffer.wrap(resp), new WriteCallback() {
|
||||
@Override
|
||||
public void writeFailed(Throwable x) {
|
||||
log.warn("Failed to send response", x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSuccess() {
|
||||
log.debug("Response sent successfully");
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.exceptionally(ex -> {
|
||||
log.error("Processing failed", ex);
|
||||
trySendCode(500);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void trySendCode(int code) {
|
||||
if (session != null && session.isOpen()) {
|
||||
byte[] resp = InboundMessageProcessor.intTo4Bytes(code);
|
||||
session.getRemote().sendBytes(ByteBuffer.wrap(resp), new WriteCallback() {
|
||||
@Override
|
||||
public void writeFailed(Throwable x) {
|
||||
log.warn("Failed to send error code", x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSuccess() {
|
||||
log.debug("Error code {} sent", code);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onClose(int statusCode, String reason) {
|
||||
log.info("WS closed: {} {}", statusCode, reason);
|
||||
// Удаляем это подключение из реестра активных соединений
|
||||
ActiveConnectionsRegistry.getInstance().remove(connectionContext);
|
||||
// На всякий случай очищаем контекст
|
||||
connectionContext.reset();
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onError(Throwable cause) {
|
||||
log.error("WS error", cause);
|
||||
}
|
||||
|
||||
// Обработка текстовых JSON-запросов
|
||||
@OnWebSocketMessage
|
||||
public void onText(String message) {
|
||||
log.info("📥 Получено TEXT-сообщение от клиента: {}", message);
|
||||
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> JsonInboundProcessor.processJson(message, connectionContext))
|
||||
.thenAccept(respJson -> {
|
||||
if (respJson != null && session != null && session.isOpen()) {
|
||||
|
||||
log.info("📤 Отправляем ответ клиенту: {}", respJson);
|
||||
|
||||
session.getRemote().sendString(respJson, new WriteCallback() {
|
||||
@Override
|
||||
public void writeFailed(Throwable x) {
|
||||
log.warn("⚠️ Не удалось отправить JSON-ответ клиенту: {}", x.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSuccess() {
|
||||
log.debug("✔ JSON-ответ успешно отправлен");
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.exceptionally(ex -> {
|
||||
log.error("❌ Ошибка при обработке JSON-сообщения", ex);
|
||||
trySendJsonError();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void trySendJsonError() {
|
||||
if (session != null && session.isOpen()) {
|
||||
String resp = "{\"op\":null,\"requestId\":null,\"status\":500,"
|
||||
+ "\"payload\":{\"code\":\"INTERNAL_ERROR\",\"message\":\"Ошибка сервера\"}}";
|
||||
|
||||
log.info("📤 Отправляем клиенту ошибку JSON: {}", resp);
|
||||
|
||||
session.getRemote().sendString(resp, new WriteCallback() {
|
||||
@Override
|
||||
public void writeFailed(Throwable x) {
|
||||
log.warn("⚠️ Не удалось отправить JSON-ответ клиенту: {}", x.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSuccess() {
|
||||
log.debug("✔ JSON-ошибка успешно отправлена");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package server.ws;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import utils.config.AppConfig;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* WsServer — поднимает Jetty WS на /ws.
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - перед стартом сервера выполняем recovery tmp-блокчейнов.
|
||||
* - если обнаружена несогласованность, которую сервер сам чинить не может —
|
||||
* recovery бросает исключение и сервер не стартует.
|
||||
*/
|
||||
public final class WsServer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WsServer.class);
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
// ============================================================
|
||||
// 0) Восстановление консистентности blockchain файлов
|
||||
// ============================================================
|
||||
try {
|
||||
BlockchainTmpRecoveryOnStartup.runRecoveryOrThrow();
|
||||
} catch (Exception e) {
|
||||
// Уже должно быть “большое” уведомление через BlockchainAdminNotifier,
|
||||
// но на всякий случай логируем ещё раз.
|
||||
log.error("❌ Сервер НЕ будет запущен: критическая ошибка восстановления blockchain tmp-файлов.", e);
|
||||
throw e; // останавливаем запуск
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 1) Настройки порта
|
||||
// ============================================================
|
||||
AppConfig config = AppConfig.getInstance();
|
||||
int port = 7070;
|
||||
try {
|
||||
String portStr = config.getParam("server.port");
|
||||
if (portStr != null && !portStr.isBlank()) {
|
||||
port = Integer.parseInt(portStr.trim());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.info("Не удалось прочитать параметр server.port, используем порт по умолчанию {}", port);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2) Запуск Jetty WS
|
||||
// ============================================================
|
||||
Server server = new Server(port);
|
||||
|
||||
ServletContextHandler context = new ServletContextHandler();
|
||||
context.setContextPath("/");
|
||||
server.setHandler(context);
|
||||
|
||||
// Инициализация контейнера WebSocket
|
||||
JettyWebSocketServletContainerInitializer.configure(context, (servletContext, wsContainer) -> {
|
||||
// Таймаут простоя соединения (Jetty 11 синтаксис)
|
||||
wsContainer.setIdleTimeout(Duration.ofMinutes(5));
|
||||
|
||||
// Маппинг эндпоинта
|
||||
wsContainer.addMapping("/ws", (req, resp) -> new BlockchainWsEndpoint());
|
||||
});
|
||||
|
||||
server.start();
|
||||
log.info("✅ WS сервер запущен на ws://localhost:{}/ws", port);
|
||||
server.join();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
SKIPFILE="skip.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# читаем список исключённых имён (без расширения) в массив
|
||||
if [[ -f "$SKIPFILE" ]]; then
|
||||
mapfile -t SKIP_LIST < "$SKIPFILE"
|
||||
else
|
||||
SKIP_LIST=()
|
||||
fi
|
||||
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
fname=$(basename "$f" .java) # имя файла без расширения
|
||||
|
||||
# проверяем, есть ли имя в списке исключений
|
||||
skip=false
|
||||
for skipf in "${SKIP_LIST[@]}"; do
|
||||
if [[ "$fname" == "$skipf" ]]; then
|
||||
skip=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$skip" == true ]]; then
|
||||
echo "Пропускаем $f"
|
||||
continue
|
||||
fi
|
||||
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
echo "Готово! Все .java файлы собраны в $OUTFILE (кроме исключённых из $SKIPFILE)"
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# OUTFILE:
|
||||
# - если пустая строка ("") -> в файл НЕ пишем, только в буфер
|
||||
# - если не пустая -> пишем в файл + (если есть wl-copy) копируем в буфер
|
||||
OUTFILE="all_files.txt"
|
||||
# OUTFILE=""
|
||||
|
||||
# === НАСТРОЙКА: перечисляй тут пути (каталоги и/или конкретные файлы) ===
|
||||
# - Если путь указывает на ФАЙЛ: берём его ВСЕГДА, даже если это не .java
|
||||
# - Если путь указывает на КАТАЛОГ: рекурсивно берём только *.java внутри
|
||||
# - Пустые строки игнорируются
|
||||
TARGETS=(
|
||||
#"./src/main/java"
|
||||
# "./server"
|
||||
# /home/ai/work/SHiNE/SHiNE-server/shine-server-blockchain
|
||||
"/home/ai/work/SHiNE/SHiNE-server/shine-server-blockchain"
|
||||
"/home/ai/work/SHiNE/SHiNE-server/shine-server-db"
|
||||
)
|
||||
|
||||
RED=$'\033[0;31m'
|
||||
RESET=$'\033[0m'
|
||||
|
||||
warn_red() {
|
||||
echo "${RED}WARN:${RESET} $*" >&2
|
||||
}
|
||||
|
||||
# временные файлы
|
||||
TMP_LIST="$(mktemp)"
|
||||
TMP_OUT="$(mktemp)"
|
||||
trap 'rm -f "$TMP_LIST" "$TMP_OUT"' EXIT
|
||||
|
||||
# собрать пути
|
||||
for path in "${TARGETS[@]}"; do
|
||||
path="$(printf '%s' "$path" | sed -e 's/^[[:space:]]\+//' -e 's/[[:space:]]\+$//')"
|
||||
[[ -z "$path" ]] && continue
|
||||
|
||||
if [[ -f "$path" ]]; then
|
||||
printf '%s\n' "$path" >> "$TMP_LIST"
|
||||
elif [[ -d "$path" ]]; then
|
||||
find "$path" -type f -name "*.java" >> "$TMP_LIST"
|
||||
else
|
||||
warn_red "Не найдено (пропускаю): $path"
|
||||
fi
|
||||
done
|
||||
|
||||
# склеиваем в TMP_OUT
|
||||
sort -u "$TMP_LIST" | while IFS= read -r f; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
warn_red "Файл исчез (пропускаю): $f"
|
||||
continue
|
||||
fi
|
||||
cat "$f" >> "$TMP_OUT"
|
||||
echo >> "$TMP_OUT"
|
||||
done
|
||||
|
||||
# если OUTFILE не пуст — пишем файл
|
||||
if [[ -n "${OUTFILE:-}" ]]; then
|
||||
: > "$OUTFILE"
|
||||
cat "$TMP_OUT" > "$OUTFILE"
|
||||
fi
|
||||
|
||||
# копирование в буфер (Wayland), если доступно
|
||||
if command -v wl-copy >/dev/null 2>&1; then
|
||||
wl-copy < "$TMP_OUT"
|
||||
else
|
||||
warn_red "wl-copy не найден — в буфер не скопировано."
|
||||
fi
|
||||
|
||||
echo "Готово!"
|
||||
if [[ -n "${OUTFILE:-}" ]]; then
|
||||
echo "Все файлы собраны в $OUTFILE"
|
||||
else
|
||||
echo "OUTFILE пуст — в файл не писали, только буфер (если wl-copy доступен)"
|
||||
fi
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# === Список файлов (ТОЛЬКО имена без расширений) ===
|
||||
# пример: Main значит Main.java, Utils значит Utils.java
|
||||
NAMES=(
|
||||
"IT_04_UserParams_NoAuth"
|
||||
"AddBlockSender"
|
||||
"ChainState"
|
||||
"JsonBuilders"
|
||||
)
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# Быстрый фильтр: сделаем хеш-таблицу из имён (ассоц. массив)
|
||||
declare -A WANT=()
|
||||
for name in "${NAMES[@]}"; do
|
||||
WANT["$name"]=1
|
||||
done
|
||||
|
||||
# собрать только нужные *.java по базовому имени
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
base="$(basename "$f" .java)"
|
||||
if [[ -n "${WANT[$base]+x}" ]]; then
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
fi
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Выбрано имён: ${#NAMES[@]}"
|
||||
echo "Все нужные .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUTFILE="all_files.txt"
|
||||
|
||||
# очищаем или создаём файл
|
||||
: > "$OUTFILE"
|
||||
|
||||
# собрать только *.java файлы и вывести их содержимое в файл
|
||||
find . -type f -name "*.java" | sort | while read -r f; do
|
||||
cat "$f" >> "$OUTFILE"
|
||||
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||
done
|
||||
|
||||
# скопировать весь файл в буфер обмена (Wayland)
|
||||
wl-copy < "$OUTFILE"
|
||||
|
||||
echo "Готово!"
|
||||
echo "Все .java файлы собраны в $OUTFILE"
|
||||
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||
@@ -160,9 +160,9 @@ public class IT_01_AddUser {
|
||||
String blockchainName = JsonParsers.userBlockchainName(resp);
|
||||
String solanaKey = JsonParsers.userSolanaKey(resp);
|
||||
String blockchainKey = JsonParsers.userBlockchainKey(resp);
|
||||
String deviceKey = JsonParsers.userDeviceKey(resp);
|
||||
String clientKey = JsonParsers.userClientKey(resp);
|
||||
|
||||
if (isBlank(login) || isBlank(blockchainName) || isBlank(solanaKey) || isBlank(blockchainKey) || isBlank(deviceKey)) {
|
||||
if (isBlank(login) || isBlank(blockchainName) || isBlank(solanaKey) || isBlank(blockchainKey) || isBlank(clientKey)) {
|
||||
r.fail("GetUser: exists=true, но поля пустые/неполные, resp=" + resp);
|
||||
fail("GetUser returned incomplete user data");
|
||||
}
|
||||
@@ -187,7 +187,7 @@ public class IT_01_AddUser {
|
||||
// ключи должны совпадать с теми, что AddUser использует при регистрации
|
||||
String expSol = TestConfig.solanaPublicKeyB64(canonical);
|
||||
String expBchKey = TestConfig.blockchainPublicKeyB64(canonical);
|
||||
String expDev = TestConfig.devicePublicKeyB64(canonical);
|
||||
String expDev = TestConfig.clientPublicKeyB64(canonical);
|
||||
|
||||
if (!solanaKey.equals(expSol)) {
|
||||
r.fail("GetUser: solanaKey mismatch, resp=" + resp);
|
||||
@@ -197,9 +197,9 @@ public class IT_01_AddUser {
|
||||
r.fail("GetUser: blockchainKey mismatch, resp=" + resp);
|
||||
fail("GetUser blockchainKey mismatch");
|
||||
}
|
||||
if (!deviceKey.equals(expDev)) {
|
||||
r.fail("GetUser: deviceKey mismatch, resp=" + resp);
|
||||
fail("GetUser deviceKey mismatch");
|
||||
if (!clientKey.equals(expDev)) {
|
||||
r.fail("GetUser: clientKey mismatch, resp=" + resp);
|
||||
fail("GetUser clientKey mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ public class IT_01_AddUser {
|
||||
"blockchainName": "%s",
|
||||
"solanaKey": "%s",
|
||||
"blockchainKey": "%s",
|
||||
"deviceKey": "%s",
|
||||
"clientKey": "%s",
|
||||
"bchLimit": %d
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
* - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
|
||||
*
|
||||
* Протокол v2:
|
||||
* - создание сессии: AuthChallenge -> CreateAuthSession (deviceKey подпись, + deviceKey + sessionKey)
|
||||
* - создание сессии: AuthChallenge -> CreateAuthSession (clientKey подпись, + clientKey + sessionKey)
|
||||
* - вход в сессию: SessionChallenge(sessionId) -> nonce, затем SessionLogin(sessionId,time,signature(sessionKey))
|
||||
* - ListSessions и CloseActiveSession доступны только в AUTH_STATUS_USER (после SessionLogin)
|
||||
*/
|
||||
@@ -122,7 +122,7 @@ public class IT_02_Sessions {
|
||||
// storagePwd на клиенте (сохраняем, чтобы потом проверить, что сервер вернул именно его)
|
||||
String storagePwd = TestConfig.fakeStoragePwd();
|
||||
|
||||
// шаг 2: CreateAuthSession (device подпись + deviceKey + sessionKey)
|
||||
// шаг 2: CreateAuthSession (device подпись + clientKey + sessionKey)
|
||||
String createResp = ws.call(
|
||||
"CreateAuthSession(" + label + ")",
|
||||
JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionMaterial.sessionKey()),
|
||||
@@ -194,7 +194,7 @@ public class IT_02_Sessions {
|
||||
"storagePwd": "%s",
|
||||
"timeMs": %d,
|
||||
"authNonce": "%s",
|
||||
"deviceKey": "%s",
|
||||
"clientKey": "%s",
|
||||
"signatureB64": "%s",
|
||||
"clientInfo": "%s"
|
||||
}
|
||||
|
||||
@@ -40,15 +40,15 @@ public class IT_04_UserParams_NoAuth {
|
||||
Duration timeout = Duration.ofSeconds(5);
|
||||
|
||||
final String login = TestConfig.LOGIN();
|
||||
final String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
|
||||
final byte[] devicePrivKey = TestConfig.getDevicePrivatKey(login);
|
||||
final String clientKeyB64 = TestConfig.clientPublicKeyB64(login);
|
||||
final byte[] clientPrivKey = TestConfig.getDevicePrivatKey(login);
|
||||
|
||||
try {
|
||||
// 1) сохранить param1
|
||||
final String p1 = "profile:name";
|
||||
final String v1 = "Anna";
|
||||
final long t1 = System.currentTimeMillis();
|
||||
upsertUserParam_OK(r, login, p1, t1, v1, deviceKeyB64, devicePrivKey, timeout);
|
||||
upsertUserParam_OK(r, login, p1, t1, v1, clientKeyB64, clientPrivKey, timeout);
|
||||
|
||||
// 2) получить param1 и проверить
|
||||
NetParam got1 = getUserParam_200(r, login, p1, timeout);
|
||||
@@ -56,7 +56,7 @@ public class IT_04_UserParams_NoAuth {
|
||||
assertEquals(p1, got1.param);
|
||||
assertEquals(t1, got1.timeMs);
|
||||
assertEquals(v1, got1.value);
|
||||
assertEquals(deviceKeyB64, got1.deviceKeyB64);
|
||||
assertEquals(clientKeyB64, got1.clientKeyB64);
|
||||
assertNotNull(got1.signatureB64);
|
||||
assertFalse(got1.signatureB64.isBlank());
|
||||
r.ok("GetUserParam(param1) OK");
|
||||
@@ -65,12 +65,12 @@ public class IT_04_UserParams_NoAuth {
|
||||
final String p2 = "profile:city";
|
||||
final String v2 = "Amsterdam";
|
||||
final long t2 = t1 + 10;
|
||||
upsertUserParam_OK(r, login, p2, t2, v2, deviceKeyB64, devicePrivKey, timeout);
|
||||
upsertUserParam_OK(r, login, p2, t2, v2, clientKeyB64, clientPrivKey, timeout);
|
||||
|
||||
// 4) обновить param1
|
||||
final String v1b = "Anna Updated";
|
||||
final long t1b = t2 + 10;
|
||||
upsertUserParam_OK(r, login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout);
|
||||
upsertUserParam_OK(r, login, p1, t1b, v1b, clientKeyB64, clientPrivKey, timeout);
|
||||
|
||||
NetParam got1b = getUserParam_200(r, login, p1, timeout);
|
||||
assertEquals(t1b, got1b.timeMs);
|
||||
@@ -92,8 +92,8 @@ public class IT_04_UserParams_NoAuth {
|
||||
assertEquals(t2, lp2.timeMs);
|
||||
assertEquals(v2, lp2.value);
|
||||
|
||||
assertEquals(deviceKeyB64, lp1.deviceKeyB64);
|
||||
assertEquals(deviceKeyB64, lp2.deviceKeyB64);
|
||||
assertEquals(clientKeyB64, lp1.clientKeyB64);
|
||||
assertEquals(clientKeyB64, lp2.clientKeyB64);
|
||||
assertNotNull(lp1.signatureB64);
|
||||
assertNotNull(lp2.signatureB64);
|
||||
|
||||
@@ -110,8 +110,8 @@ public class IT_04_UserParams_NoAuth {
|
||||
// WS helpers: Upsert/Get/List
|
||||
// =================================================================================
|
||||
|
||||
private static void upsertUserParam_OK(TestResult r, String login, String param, long timeMs, String value, String deviceKeyB64, byte[] devicePrivKey, Duration timeout) {
|
||||
String signatureB64 = signUserParam(devicePrivKey, login, param, timeMs, value);
|
||||
private static void upsertUserParam_OK(TestResult r, String login, String param, long timeMs, String value, String clientKeyB64, byte[] clientPrivKey, Duration timeout) {
|
||||
String signatureB64 = signUserParam(clientPrivKey, login, param, timeMs, value);
|
||||
|
||||
String reqJson = """
|
||||
{
|
||||
@@ -122,11 +122,11 @@ public class IT_04_UserParams_NoAuth {
|
||||
"param": "%s",
|
||||
"time_ms": %d,
|
||||
"value": "%s",
|
||||
"device_key": "%s",
|
||||
"client_key": "%s",
|
||||
"signature": "%s"
|
||||
}
|
||||
}
|
||||
""".formatted(TestIds.next("upsert"), login, param, timeMs, jsonEscape(value), deviceKeyB64, signatureB64);
|
||||
""".formatted(TestIds.next("upsert"), login, param, timeMs, jsonEscape(value), clientKeyB64, signatureB64);
|
||||
|
||||
try (WsSession ws = WsSession.open()) {
|
||||
String resp = ws.call("UpsertUserParam(" + param + ")", reqJson, timeout);
|
||||
@@ -187,7 +187,7 @@ public class IT_04_UserParams_NoAuth {
|
||||
p.param = text(payload, "param");
|
||||
p.timeMs = longVal(payload, "time_ms");
|
||||
p.value = text(payload, "value");
|
||||
p.deviceKeyB64 = text(payload, "device_key");
|
||||
p.clientKeyB64 = text(payload, "client_key");
|
||||
p.signatureB64 = text(payload, "signature");
|
||||
return p;
|
||||
} catch (Exception e) {
|
||||
@@ -214,7 +214,7 @@ public class IT_04_UserParams_NoAuth {
|
||||
p.param = text(it, "param");
|
||||
p.timeMs = longVal(it, "time_ms");
|
||||
p.value = text(it, "value");
|
||||
p.deviceKeyB64 = text(it, "device_key");
|
||||
p.clientKeyB64 = text(it, "client_key");
|
||||
p.signatureB64 = text(it, "signature");
|
||||
out.items = out.itemsAppend(p);
|
||||
}
|
||||
@@ -239,10 +239,10 @@ public class IT_04_UserParams_NoAuth {
|
||||
// Signature + JSON helpers
|
||||
// =================================================================================
|
||||
|
||||
private static String signUserParam(byte[] devicePrivKey, String login, String param, long timeMs, String value) {
|
||||
private static String signUserParam(byte[] clientPrivKey, String login, String param, long timeMs, String value) {
|
||||
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX + login + param + timeMs + value;
|
||||
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] sig64 = Ed25519Util.sign(signBytes, devicePrivKey);
|
||||
byte[] sig64 = Ed25519Util.sign(signBytes, clientPrivKey);
|
||||
return Base64.getEncoder().encodeToString(sig64);
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ public class IT_04_UserParams_NoAuth {
|
||||
String param;
|
||||
long timeMs;
|
||||
String value;
|
||||
String deviceKeyB64;
|
||||
String clientKeyB64;
|
||||
String signatureB64;
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ public class IT_07_EspPairing {
|
||||
entry.setBlockchainName(TestConfig.getBlockchainName(LOGIN));
|
||||
entry.setSolanaKey(TestConfig.solanaPublicKeyB64(LOGIN));
|
||||
entry.setBlockchainKey(TestConfig.blockchainPublicKeyB64(LOGIN));
|
||||
entry.setDeviceKey(TestConfig.devicePublicKeyB64(LOGIN));
|
||||
entry.setClientKey(TestConfig.clientPublicKeyB64(LOGIN));
|
||||
SolanaUsersDAO.getInstance().insert(entry);
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ public final class SeedDataPopulationHelper {
|
||||
"blockchainName": "%s",
|
||||
"solanaKey": "%s",
|
||||
"blockchainKey": "%s",
|
||||
"deviceKey": "%s",
|
||||
"clientKey": "%s",
|
||||
"bchLimit": 50000000
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@ public final class SeedDataPopulationHelper {
|
||||
bch(login),
|
||||
keys.solanaPublicB64,
|
||||
keys.blockchainPublicB64,
|
||||
keys.devicePublicB64
|
||||
keys.clientPublicB64
|
||||
);
|
||||
|
||||
String resp = ws.call("AddUser#" + login, req, timeout);
|
||||
@@ -244,12 +244,11 @@ public final class SeedDataPopulationHelper {
|
||||
}
|
||||
|
||||
private static UserKeys deriveKeysFromPassword(String password) {
|
||||
byte[] base = HashSHA256Util.sha256(password.getBytes(StandardCharsets.UTF_8));
|
||||
String baseB64 = Base64.getEncoder().encodeToString(base);
|
||||
byte[] masterSecret = HashSHA256Util.sha256(password.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
byte[] rootPriv = HashSHA256Util.sha256((baseB64 + "root.key").getBytes(StandardCharsets.UTF_8));
|
||||
byte[] bchPriv = HashSHA256Util.sha256((baseB64 + "bch.key").getBytes(StandardCharsets.UTF_8));
|
||||
byte[] devPriv = HashSHA256Util.sha256((baseB64 + "dev.key").getBytes(StandardCharsets.UTF_8));
|
||||
byte[] rootPriv = deriveSeed(masterSecret, "root.key");
|
||||
byte[] bchPriv = deriveSeed(masterSecret, "blockchain.key");
|
||||
byte[] devPriv = deriveSeed(masterSecret, "client.key");
|
||||
|
||||
String rootPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(rootPriv));
|
||||
String bchPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(bchPriv));
|
||||
@@ -258,6 +257,21 @@ public final class SeedDataPopulationHelper {
|
||||
return new UserKeys(rootPubB64, bchPubB64, devPubB64, bchPriv);
|
||||
}
|
||||
|
||||
private static byte[] deriveSeed(byte[] masterSecret, String suffix) {
|
||||
byte[] prefix = "SHiNE-key".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] suffixBytes = suffix.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] material = new byte[prefix.length + 1 + masterSecret.length + 1 + suffixBytes.length];
|
||||
int offset = 0;
|
||||
System.arraycopy(prefix, 0, material, offset, prefix.length);
|
||||
offset += prefix.length;
|
||||
material[offset++] = 0;
|
||||
System.arraycopy(masterSecret, 0, material, offset, masterSecret.length);
|
||||
offset += masterSecret.length;
|
||||
material[offset++] = 0;
|
||||
System.arraycopy(suffixBytes, 0, material, offset, suffixBytes.length);
|
||||
return HashSHA256Util.sha256(material);
|
||||
}
|
||||
|
||||
public static final class UserSpec {
|
||||
public final String login;
|
||||
public final String firstName;
|
||||
@@ -292,16 +306,16 @@ public final class SeedDataPopulationHelper {
|
||||
private static final class UserKeys {
|
||||
final String solanaPublicB64;
|
||||
final String blockchainPublicB64;
|
||||
final String devicePublicB64;
|
||||
final String clientPublicB64;
|
||||
final byte[] blockchainPrivate32;
|
||||
|
||||
private UserKeys(String solanaPublicB64,
|
||||
String blockchainPublicB64,
|
||||
String devicePublicB64,
|
||||
String clientPublicB64,
|
||||
byte[] blockchainPrivate32) {
|
||||
this.solanaPublicB64 = solanaPublicB64;
|
||||
this.blockchainPublicB64 = blockchainPublicB64;
|
||||
this.devicePublicB64 = devicePublicB64;
|
||||
this.clientPublicB64 = clientPublicB64;
|
||||
this.blockchainPrivate32 = blockchainPrivate32;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ public final class TestConfig {
|
||||
}
|
||||
|
||||
// ============ key maps ============
|
||||
private static final Map<String, byte[]> devicePriv = new ConcurrentHashMap<>();
|
||||
private static final Map<String, byte[]> devicePub = new ConcurrentHashMap<>();
|
||||
private static final Map<String, byte[]> clientPriv = new ConcurrentHashMap<>();
|
||||
private static final Map<String, byte[]> clientPub = new ConcurrentHashMap<>();
|
||||
|
||||
private static final Map<String, byte[]> solanaPriv = new ConcurrentHashMap<>();
|
||||
private static final Map<String, byte[]> solanaPub = new ConcurrentHashMap<>();
|
||||
@@ -80,8 +80,8 @@ public final class TestConfig {
|
||||
byte[] pub = Ed25519Util.derivePublicKey(priv);
|
||||
|
||||
// пока одинаковые
|
||||
devicePriv.put(login, priv);
|
||||
devicePub.put(login, pub);
|
||||
clientPriv.put(login, priv);
|
||||
clientPub.put(login, pub);
|
||||
|
||||
solanaPriv.put(login, priv);
|
||||
solanaPub.put(login, pub);
|
||||
@@ -99,8 +99,8 @@ public final class TestConfig {
|
||||
|
||||
// ============ requested getters (with your names) ============
|
||||
|
||||
public static byte[] getDevicePrivatKey(String login) { return cloneOrThrow(devicePriv.get(login), "devicePriv", login); }
|
||||
public static byte[] getDevicePublicKey(String login) { return cloneOrThrow(devicePub.get(login), "devicePub", login); }
|
||||
public static byte[] getDevicePrivatKey(String login) { return cloneOrThrow(clientPriv.get(login), "clientPriv", login); }
|
||||
public static byte[] getDevicePublicKey(String login) { return cloneOrThrow(clientPub.get(login), "clientPub", login); }
|
||||
|
||||
public static byte[] getSolanaPrivatKey(String login) { return cloneOrThrow(solanaPriv.get(login), "solanaPriv", login); }
|
||||
public static byte[] getSolanaPublicKey(String login) { return cloneOrThrow(solanaPub.get(login), "solanaPub", login); }
|
||||
@@ -113,7 +113,7 @@ public final class TestConfig {
|
||||
public static byte[] getSessionPublicKey(String login) { return cloneOrThrow(sessionPub.get(login), "sessionPub", login); }
|
||||
|
||||
// ============ base64 helpers ============
|
||||
public static String devicePublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getDevicePublicKey(login)); }
|
||||
public static String clientPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getDevicePublicKey(login)); }
|
||||
public static String solanaPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getSolanaPublicKey(login)); }
|
||||
public static String blockchainPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getBlockchainPublicKey(login)); }
|
||||
|
||||
@@ -136,9 +136,9 @@ public final class TestConfig {
|
||||
public static String BLOCKCHAIN2_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN2()); }
|
||||
public static String BLOCKCHAIN3_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN3()); }
|
||||
|
||||
public static String DEVICE_PUBKEY_B64() { return devicePublicKeyB64(LOGIN()); }
|
||||
public static String DEVICE2_PUBKEY_B64() { return devicePublicKeyB64(LOGIN2()); }
|
||||
public static String DEVICE3_PUBKEY_B64() { return devicePublicKeyB64(LOGIN3()); }
|
||||
public static String DEVICE_PUBKEY_B64() { return clientPublicKeyB64(LOGIN()); }
|
||||
public static String DEVICE2_PUBKEY_B64() { return clientPublicKeyB64(LOGIN2()); }
|
||||
public static String DEVICE3_PUBKEY_B64() { return clientPublicKeyB64(LOGIN3()); }
|
||||
|
||||
// NEW: session pub b64 compat
|
||||
public static String SESSION_PUBKEY_B64() { return sessionPublicKeyB64(LOGIN()); }
|
||||
|
||||
@@ -19,7 +19,7 @@ public final class JsonBuilders {
|
||||
|
||||
String solanaKeyB64 = TestConfig.solanaPublicKeyB64(login);
|
||||
String blockchainKeyB64 = TestConfig.blockchainPublicKeyB64(login);
|
||||
String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
|
||||
String clientKeyB64 = TestConfig.clientPublicKeyB64(login);
|
||||
|
||||
return """
|
||||
{
|
||||
@@ -30,7 +30,7 @@ public final class JsonBuilders {
|
||||
"blockchainName": "%s",
|
||||
"solanaKey": "%s",
|
||||
"blockchainKey": "%s",
|
||||
"deviceKey": "%s",
|
||||
"clientKey": "%s",
|
||||
"bchLimit": %d
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public final class JsonBuilders {
|
||||
blockchainName,
|
||||
solanaKeyB64,
|
||||
blockchainKeyB64,
|
||||
deviceKeyB64,
|
||||
clientKeyB64,
|
||||
TestConfig.TEST_BCH_LIMIT
|
||||
);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public final class JsonBuilders {
|
||||
}
|
||||
|
||||
// ---------------- CreateAuthSession (v2) ----------------
|
||||
// Подпись CreateAuthSession делается deviceKey над строкой:
|
||||
// Подпись CreateAuthSession делается clientKey над строкой:
|
||||
// preimage = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce
|
||||
|
||||
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionKey) {
|
||||
@@ -143,9 +143,9 @@ public final class JsonBuilders {
|
||||
public static String createAuthSessionV2(String login, String authNonce, String storagePwd, String sessionKey, int sessionType, String clientPlatform) {
|
||||
long timeMs = System.currentTimeMillis();
|
||||
|
||||
byte[] devicePriv = TestConfig.getDevicePrivatKey(login);
|
||||
String deviceKey = TestConfig.devicePublicKeyB64(login);
|
||||
String sigB64 = signAuthCreateSession(login, sessionKey, storagePwd, timeMs, authNonce, devicePriv);
|
||||
byte[] clientPriv = TestConfig.getDevicePrivatKey(login);
|
||||
String clientKey = TestConfig.clientPublicKeyB64(login);
|
||||
String sigB64 = signAuthCreateSession(login, sessionKey, storagePwd, timeMs, authNonce, clientPriv);
|
||||
|
||||
String requestId = TestIds.next("create");
|
||||
return """
|
||||
@@ -158,7 +158,7 @@ public final class JsonBuilders {
|
||||
"sessionKey": "%s",
|
||||
"timeMs": %d,
|
||||
"authNonce": "%s",
|
||||
"deviceKey": "%s",
|
||||
"clientKey": "%s",
|
||||
"signatureB64": "%s",
|
||||
"sessionType": %d,
|
||||
"clientPlatform": "%s",
|
||||
@@ -172,7 +172,7 @@ public final class JsonBuilders {
|
||||
sessionKey,
|
||||
timeMs,
|
||||
authNonce,
|
||||
deviceKey,
|
||||
clientKey,
|
||||
sigB64,
|
||||
sessionType,
|
||||
clientPlatform == null ? "" : clientPlatform,
|
||||
@@ -431,12 +431,12 @@ public final class JsonBuilders {
|
||||
/**
|
||||
* Подпись CreateAuthSession(v2):
|
||||
* preimage = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce
|
||||
* подписываем devicePrivKey.
|
||||
* подписываем clientPrivKey.
|
||||
*/
|
||||
public static String signAuthCreateSession(String login, String sessionKey, String storagePwd, long timeMs, String authNonce, byte[] devicePrivKey) {
|
||||
public static String signAuthCreateSession(String login, String sessionKey, String storagePwd, long timeMs, String authNonce, byte[] clientPrivKey) {
|
||||
String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + sessionKey + ":" + storagePwd + ":" + timeMs + ":" + authNonce;
|
||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
|
||||
byte[] sig = Ed25519Util.sign(preimage, clientPrivKey);
|
||||
return Base64.getEncoder().encodeToString(sig);
|
||||
}
|
||||
|
||||
|
||||
@@ -229,8 +229,8 @@ public final class JsonParsers {
|
||||
return getPayloadText(json, "blockchainKey");
|
||||
}
|
||||
|
||||
public static String userDeviceKey(String json) {
|
||||
return getPayloadText(json, "deviceKey");
|
||||
public static String userClientKey(String json) {
|
||||
return getPayloadText(json, "clientKey");
|
||||
}
|
||||
|
||||
// ---------------- SearchUsers helpers ----------------
|
||||
|
||||
Reference in New Issue
Block a user