API сессий: добавить sessionType и clientPlatform
This commit is contained in:
parent
3b8ea70d3c
commit
919387f581
@ -17,6 +17,23 @@
|
|||||||
- на втором шаге клиент присылает подписанный ответ;
|
- на втором шаге клиент присылает подписанный ответ;
|
||||||
- сервер сверяет актуальные публичные ключи и только потом проверяет подпись.
|
- сервер сверяет актуальные публичные ключи и только потом проверяет подпись.
|
||||||
|
|
||||||
|
Новые поля этого раздела:
|
||||||
|
|
||||||
|
- `sessionType` — числовой код типа сессии;
|
||||||
|
- `clientPlatform` — свободная строка платформы клиента.
|
||||||
|
|
||||||
|
Текущие поддерживаемые коды `sessionType`:
|
||||||
|
|
||||||
|
- `1` — обычный клиент;
|
||||||
|
- `50` — кошелёк;
|
||||||
|
- `100` — homeserver.
|
||||||
|
|
||||||
|
Правило проверки `sessionType`:
|
||||||
|
|
||||||
|
1. если в `Solana PDA` нет записи для `sessionKey`, сервер принимает `sessionType`, присланный клиентом;
|
||||||
|
2. если запись в `PDA` есть, `sessionType` в запросе должен совпадать с `session_type` из `PDA`;
|
||||||
|
3. при несовпадении сервер возвращает `460 / SESSION_TYPE_MISMATCH`.
|
||||||
|
|
||||||
Ниже в документе сначала описан сценарий, а потом зафиксированы точные форматы запросов и ответов.
|
Ниже в документе сначала описан сценарий, а потом зафиксированы точные форматы запросов и ответов.
|
||||||
|
|
||||||
## 1. Поток авторизации
|
## 1. Поток авторизации
|
||||||
@ -94,6 +111,8 @@ ed25519/BASE64_PUBLIC_KEY
|
|||||||
"authNonce": "nonce",
|
"authNonce": "nonce",
|
||||||
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
|
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
"signatureB64": "BASE64_SIGNATURE",
|
"signatureB64": "BASE64_SIGNATURE",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
"clientInfo": "Android 15; Pixel 9"
|
"clientInfo": "Android 15; Pixel 9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,6 +172,8 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
|||||||
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`.
|
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`.
|
||||||
- `422 / DEVICE_KEY_NOT_ACTUAL` — `deviceKey` не совпадает с актуальной версией на сервере.
|
- `422 / DEVICE_KEY_NOT_ACTUAL` — `deviceKey` не совпадает с актуальной версией на сервере.
|
||||||
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||||||
|
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||||||
|
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
|
||||||
- `501 / DB_ERROR_SESSION_CREATE` — ошибка БД при создании записи активной сессии.
|
- `501 / DB_ERROR_SESSION_CREATE` — ошибка БД при создании записи активной сессии.
|
||||||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
@ -208,6 +229,8 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
|||||||
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||||
"timeMs": 1774600010456,
|
"timeMs": 1774600010456,
|
||||||
"signatureB64": "BASE64_SIGNATURE",
|
"signatureB64": "BASE64_SIGNATURE",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
"clientInfo": "Android 15; Pixel 9"
|
"clientInfo": "Android 15; Pixel 9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,6 +281,8 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
|||||||
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` не поддерживается текущим сервером.
|
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` не поддерживается текущим сервером.
|
||||||
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey` или `signatureB64`.
|
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey` или `signatureB64`.
|
||||||
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||||||
|
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||||||
|
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
|
||||||
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при чтении пользователя для этой сессии.
|
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при чтении пользователя для этой сессии.
|
||||||
- `422 / USER_NOT_FOUND_FOR_SESSION` — пользователь, которому принадлежит сессия, не найден.
|
- `422 / USER_NOT_FOUND_FOR_SESSION` — пользователь, которому принадлежит сессия, не найден.
|
||||||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|||||||
@ -42,6 +42,8 @@
|
|||||||
"sessions": [
|
"sessions": [
|
||||||
{
|
{
|
||||||
"sessionId": "sess_7c5e5c4b",
|
"sessionId": "sess_7c5e5c4b",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
"clientInfoFromClient": "Android 15; Pixel 9",
|
"clientInfoFromClient": "Android 15; Pixel 9",
|
||||||
"clientInfoFromRequest": "UA=Java-http-client/17.0.18; remote=127.0.0.1",
|
"clientInfoFromRequest": "UA=Java-http-client/17.0.18; remote=127.0.0.1",
|
||||||
"geo": "RU/Moscow",
|
"geo": "RU/Moscow",
|
||||||
@ -58,6 +60,19 @@
|
|||||||
- `501 / DB_ERROR_LIST_SESSIONS` — ошибка БД при чтении списка активных сессий.
|
- `501 / DB_ERROR_LIST_SESSIONS` — ошибка БД при чтении списка активных сессий.
|
||||||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
### Поля одной сессии в `ListSessions`
|
||||||
|
|
||||||
|
- `sessionId` — идентификатор активной сессии;
|
||||||
|
- `sessionType` — числовой код типа сессии:
|
||||||
|
- `1` — клиент;
|
||||||
|
- `50` — кошелёк;
|
||||||
|
- `100` — homeserver;
|
||||||
|
- `clientPlatform` — строка платформы, как её прислал клиент;
|
||||||
|
- `clientInfoFromClient` — краткая строка клиента;
|
||||||
|
- `clientInfoFromRequest` — строка, собранная сервером из запроса;
|
||||||
|
- `geo` — страна/город или fallback-строка;
|
||||||
|
- `lastAuthenticatedAtMs` — время последней успешной авторизации этой сессии.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. `CloseActiveSession`
|
## 2. `CloseActiveSession`
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
# sessionType API и UI
|
||||||
|
|
||||||
|
- краткое описание:
|
||||||
|
- серверный API авторизации и списка сессий расширен полями `sessionType` и `clientPlatform`;
|
||||||
|
- сервер сверяет `sessionType` с записью в Solana `PDA`, если для данного `sessionKey` уже есть `SessionRecord`;
|
||||||
|
- `shine-UI` теперь отправляет `sessionType=1` и `clientPlatform=Web`, а также показывает тип/платформу в экранах сессий;
|
||||||
|
- в документации Solana `PDA` добавлен тип `50` для `wallet`.
|
||||||
|
|
||||||
|
- что проверять:
|
||||||
|
- вход через `CreateAuthSession` и повторный вход через `SessionLogin`;
|
||||||
|
- ответ `ListSessions` должен содержать `sessionType` и `clientPlatform`;
|
||||||
|
- в UI "Устройства" должен отображаться тип сессии и платформа;
|
||||||
|
- при искусственном несовпадении `sessionType` с `PDA` сервер должен вернуть `460 / SESSION_TYPE_MISMATCH`.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- обычный web-клиент успешно создаёт/возобновляет сессию с `sessionType=1`;
|
||||||
|
- список сессий возвращает новые поля;
|
||||||
|
- несовпадение типа с `PDA` даёт явную прикладную ошибку `460`.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- in_progress
|
||||||
@ -178,6 +178,8 @@ public final class DatabaseInitializer {
|
|||||||
client_ip TEXT,
|
client_ip TEXT,
|
||||||
client_info_from_client TEXT,
|
client_info_from_client TEXT,
|
||||||
client_info_from_request TEXT,
|
client_info_from_request TEXT,
|
||||||
|
session_type INTEGER NOT NULL DEFAULT 1,
|
||||||
|
client_platform TEXT NOT NULL DEFAULT '',
|
||||||
user_language TEXT,
|
user_language TEXT,
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import java.sql.Statement;
|
|||||||
public final class SqliteDbController {
|
public final class SqliteDbController {
|
||||||
|
|
||||||
private static volatile SqliteDbController instance;
|
private static volatile SqliteDbController instance;
|
||||||
private static final int LATEST_SCHEMA_VERSION = 3;
|
private static final int LATEST_SCHEMA_VERSION = 4;
|
||||||
|
|
||||||
private final String jdbcUrl;
|
private final String jdbcUrl;
|
||||||
|
|
||||||
@ -86,6 +86,7 @@ public final class SqliteDbController {
|
|||||||
case 1 -> migrateToV1();
|
case 1 -> migrateToV1();
|
||||||
case 2 -> migrateToV2();
|
case 2 -> migrateToV2();
|
||||||
case 3 -> migrateToV3();
|
case 3 -> migrateToV3();
|
||||||
|
case 4 -> migrateToV4();
|
||||||
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
|
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,6 +169,26 @@ public final class SqliteDbController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void migrateToV4() {
|
||||||
|
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||||
|
Statement st = c.createStatement()) {
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
ensureActiveSessionsSessionTypeColumn(c, st);
|
||||||
|
ensureActiveSessionsClientPlatformColumn(c, st);
|
||||||
|
setSchemaVersion(c, 4);
|
||||||
|
c.commit();
|
||||||
|
} catch (Exception e) {
|
||||||
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
|
throw new RuntimeException("DB migration to v4 failed", e);
|
||||||
|
} finally {
|
||||||
|
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("DB migration to v4 failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ensureChat200StateTables(Statement st) throws SQLException {
|
private static void ensureChat200StateTables(Statement st) throws SQLException {
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS chat200_state (
|
CREATE TABLE IF NOT EXISTS chat200_state (
|
||||||
@ -235,6 +256,28 @@ public final class SqliteDbController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ensureActiveSessionsSessionTypeColumn(Connection c, Statement st) throws SQLException {
|
||||||
|
if (columnExists(c, "active_sessions", "session_type")) return;
|
||||||
|
st.executeUpdate("ALTER TABLE active_sessions ADD COLUMN session_type INTEGER NOT NULL DEFAULT 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureActiveSessionsClientPlatformColumn(Connection c, Statement st) throws SQLException {
|
||||||
|
if (columnExists(c, "active_sessions", "client_platform")) return;
|
||||||
|
st.executeUpdate("ALTER TABLE active_sessions ADD COLUMN client_platform TEXT NOT NULL DEFAULT ''");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean columnExists(Connection c, String tableName, String columnName) throws SQLException {
|
||||||
|
try (Statement probe = c.createStatement();
|
||||||
|
ResultSet rs = probe.executeQuery("PRAGMA table_info(" + tableName + ")")) {
|
||||||
|
while (rs.next()) {
|
||||||
|
if (columnName.equalsIgnoreCase(rs.getString("name"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void setSchemaVersion(Connection c, int version) throws SQLException {
|
private static void setSchemaVersion(Connection c, int version) throws SQLException {
|
||||||
try (var ps = c.prepareStatement("""
|
try (var ps = c.prepareStatement("""
|
||||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
|||||||
@ -47,8 +47,10 @@ public final class ActiveSessionsDAO {
|
|||||||
client_ip,
|
client_ip,
|
||||||
client_info_from_client,
|
client_info_from_client,
|
||||||
client_info_from_request,
|
client_info_from_request,
|
||||||
|
session_type,
|
||||||
|
client_platform,
|
||||||
user_language
|
user_language
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@ -64,7 +66,9 @@ public final class ActiveSessionsDAO {
|
|||||||
ps.setString(10, session.getClientIp());
|
ps.setString(10, session.getClientIp());
|
||||||
ps.setString(11, session.getClientInfoFromClient());
|
ps.setString(11, session.getClientInfoFromClient());
|
||||||
ps.setString(12, session.getClientInfoFromRequest());
|
ps.setString(12, session.getClientInfoFromRequest());
|
||||||
ps.setString(13, session.getUserLanguage());
|
ps.setInt(13, session.getSessionType());
|
||||||
|
ps.setString(14, session.getClientPlatform());
|
||||||
|
ps.setString(15, session.getUserLanguage());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,6 +96,8 @@ public final class ActiveSessionsDAO {
|
|||||||
client_ip,
|
client_ip,
|
||||||
client_info_from_client,
|
client_info_from_client,
|
||||||
client_info_from_request,
|
client_info_from_request,
|
||||||
|
session_type,
|
||||||
|
client_platform,
|
||||||
user_language
|
user_language
|
||||||
FROM active_sessions
|
FROM active_sessions
|
||||||
WHERE session_id = ?
|
WHERE session_id = ?
|
||||||
@ -127,6 +133,8 @@ public final class ActiveSessionsDAO {
|
|||||||
client_ip,
|
client_ip,
|
||||||
client_info_from_client,
|
client_info_from_client,
|
||||||
client_info_from_request,
|
client_info_from_request,
|
||||||
|
session_type,
|
||||||
|
client_platform,
|
||||||
user_language
|
user_language
|
||||||
FROM active_sessions
|
FROM active_sessions
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE login = ? COLLATE NOCASE
|
||||||
@ -179,6 +187,8 @@ public final class ActiveSessionsDAO {
|
|||||||
String clientIp,
|
String clientIp,
|
||||||
String clientInfoFromClient,
|
String clientInfoFromClient,
|
||||||
String clientInfoFromRequest,
|
String clientInfoFromRequest,
|
||||||
|
int sessionType,
|
||||||
|
String clientPlatform,
|
||||||
String userLanguage
|
String userLanguage
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
|
|
||||||
@ -189,6 +199,8 @@ public final class ActiveSessionsDAO {
|
|||||||
client_ip = ?,
|
client_ip = ?,
|
||||||
client_info_from_client = ?,
|
client_info_from_client = ?,
|
||||||
client_info_from_request = ?,
|
client_info_from_request = ?,
|
||||||
|
session_type = ?,
|
||||||
|
client_platform = ?,
|
||||||
user_language = ?
|
user_language = ?
|
||||||
WHERE session_id = ?
|
WHERE session_id = ?
|
||||||
""";
|
""";
|
||||||
@ -198,8 +210,10 @@ public final class ActiveSessionsDAO {
|
|||||||
ps.setString(2, clientIp);
|
ps.setString(2, clientIp);
|
||||||
ps.setString(3, clientInfoFromClient);
|
ps.setString(3, clientInfoFromClient);
|
||||||
ps.setString(4, clientInfoFromRequest);
|
ps.setString(4, clientInfoFromRequest);
|
||||||
ps.setString(5, userLanguage);
|
ps.setInt(5, sessionType);
|
||||||
ps.setString(6, sessionId);
|
ps.setString(6, clientPlatform);
|
||||||
|
ps.setString(7, userLanguage);
|
||||||
|
ps.setString(8, sessionId);
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,10 +224,12 @@ public final class ActiveSessionsDAO {
|
|||||||
String clientIp,
|
String clientIp,
|
||||||
String clientInfoFromClient,
|
String clientInfoFromClient,
|
||||||
String clientInfoFromRequest,
|
String clientInfoFromRequest,
|
||||||
|
int sessionType,
|
||||||
|
String clientPlatform,
|
||||||
String userLanguage
|
String userLanguage
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
updateOnRefresh(c, sessionId, lastAuthMs, clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage);
|
updateOnRefresh(c, sessionId, lastAuthMs, clientIp, clientInfoFromClient, clientInfoFromRequest, sessionType, clientPlatform, userLanguage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,6 +284,8 @@ public final class ActiveSessionsDAO {
|
|||||||
String clientIp = rs.getString("client_ip");
|
String clientIp = rs.getString("client_ip");
|
||||||
String clientInfoFromClient = rs.getString("client_info_from_client");
|
String clientInfoFromClient = rs.getString("client_info_from_client");
|
||||||
String clientInfoFromRequest = rs.getString("client_info_from_request");
|
String clientInfoFromRequest = rs.getString("client_info_from_request");
|
||||||
|
int sessionType = rs.getInt("session_type");
|
||||||
|
String clientPlatform = rs.getString("client_platform");
|
||||||
String userLanguage = rs.getString("user_language");
|
String userLanguage = rs.getString("user_language");
|
||||||
|
|
||||||
return new ActiveSessionEntry(
|
return new ActiveSessionEntry(
|
||||||
@ -283,6 +301,8 @@ public final class ActiveSessionsDAO {
|
|||||||
clientIp,
|
clientIp,
|
||||||
clientInfoFromClient,
|
clientInfoFromClient,
|
||||||
clientInfoFromRequest,
|
clientInfoFromRequest,
|
||||||
|
sessionType,
|
||||||
|
clientPlatform,
|
||||||
userLanguage
|
userLanguage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,8 @@ public class ActiveSessionEntry {
|
|||||||
private String clientIp;
|
private String clientIp;
|
||||||
private String clientInfoFromClient;
|
private String clientInfoFromClient;
|
||||||
private String clientInfoFromRequest;
|
private String clientInfoFromRequest;
|
||||||
|
private int sessionType;
|
||||||
|
private String clientPlatform;
|
||||||
private String userLanguage;
|
private String userLanguage;
|
||||||
|
|
||||||
public ActiveSessionEntry() { }
|
public ActiveSessionEntry() { }
|
||||||
@ -38,6 +40,8 @@ public class ActiveSessionEntry {
|
|||||||
String clientIp,
|
String clientIp,
|
||||||
String clientInfoFromClient,
|
String clientInfoFromClient,
|
||||||
String clientInfoFromRequest,
|
String clientInfoFromRequest,
|
||||||
|
int sessionType,
|
||||||
|
String clientPlatform,
|
||||||
String userLanguage) {
|
String userLanguage) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.login = login;
|
this.login = login;
|
||||||
@ -51,6 +55,8 @@ public class ActiveSessionEntry {
|
|||||||
this.clientIp = clientIp;
|
this.clientIp = clientIp;
|
||||||
this.clientInfoFromClient = clientInfoFromClient;
|
this.clientInfoFromClient = clientInfoFromClient;
|
||||||
this.clientInfoFromRequest = clientInfoFromRequest;
|
this.clientInfoFromRequest = clientInfoFromRequest;
|
||||||
|
this.sessionType = sessionType;
|
||||||
|
this.clientPlatform = clientPlatform;
|
||||||
this.userLanguage = userLanguage;
|
this.userLanguage = userLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +96,12 @@ public class ActiveSessionEntry {
|
|||||||
public String getClientInfoFromRequest() { return clientInfoFromRequest; }
|
public String getClientInfoFromRequest() { return clientInfoFromRequest; }
|
||||||
public void setClientInfoFromRequest(String clientInfoFromRequest) { this.clientInfoFromRequest = clientInfoFromRequest; }
|
public void setClientInfoFromRequest(String clientInfoFromRequest) { this.clientInfoFromRequest = clientInfoFromRequest; }
|
||||||
|
|
||||||
|
public int getSessionType() { return sessionType; }
|
||||||
|
public void setSessionType(int sessionType) { this.sessionType = sessionType; }
|
||||||
|
|
||||||
|
public String getClientPlatform() { return clientPlatform; }
|
||||||
|
public void setClientPlatform(String clientPlatform) { this.clientPlatform = clientPlatform; }
|
||||||
|
|
||||||
public String getUserLanguage() { return userLanguage; }
|
public String getUserLanguage() { return userLanguage; }
|
||||||
public void setUserLanguage(String userLanguage) { this.userLanguage = userLanguage; }
|
public void setUserLanguage(String userLanguage) { this.userLanguage = userLanguage; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
final class AuthSessionTypeSupport {
|
||||||
|
|
||||||
|
static final int SESSION_TYPE_CLIENT = 1;
|
||||||
|
static final int SESSION_TYPE_WALLET = 50;
|
||||||
|
static final int SESSION_TYPE_HOMESERVER = 100;
|
||||||
|
static final int SESSION_TYPE_MISMATCH_STATUS = 460;
|
||||||
|
|
||||||
|
private AuthSessionTypeSupport() {}
|
||||||
|
|
||||||
|
static int normalizeRequestedSessionType(Integer rawType) {
|
||||||
|
return rawType == null ? SESSION_TYPE_CLIENT : rawType.intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isSupportedSessionType(int sessionType) {
|
||||||
|
return sessionType == SESSION_TYPE_CLIENT
|
||||||
|
|| sessionType == SESSION_TYPE_WALLET
|
||||||
|
|| sessionType == SESSION_TYPE_HOMESERVER;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String normalizeClientPlatform(String clientPlatform) {
|
||||||
|
if (clientPlatform == null) return "";
|
||||||
|
String trimmed = clientPlatform.trim();
|
||||||
|
if (trimmed.length() <= 64) return trimmed;
|
||||||
|
return trimmed.substring(0, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] tryParseSessionPublicKey32(String sessionKeyApi) {
|
||||||
|
if (sessionKeyApi == null || sessionKeyApi.isBlank()) return null;
|
||||||
|
try {
|
||||||
|
return AuthKeyUtils.parseEd25519PublicKey(sessionKeyApi, "sessionKey");
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
try {
|
||||||
|
byte[] raw = Base64.getDecoder().decode(sessionKeyApi.trim());
|
||||||
|
return raw.length == 32 ? raw : null;
|
||||||
|
} catch (Exception ignoredToo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -213,6 +213,19 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int requestedSessionType = AuthSessionTypeSupport.normalizeRequestedSessionType(req.getSessionType());
|
||||||
|
if (!AuthSessionTypeSupport.isSupportedSessionType(requestedSessionType)) {
|
||||||
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_SESSION_TYPE",
|
||||||
|
"Неподдерживаемый sessionType"
|
||||||
|
);
|
||||||
|
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: bad sessionType");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getClientPlatform());
|
||||||
|
|
||||||
String deviceKeyFromDb = user.getDeviceKey();
|
String deviceKeyFromDb = user.getDeviceKey();
|
||||||
if (deviceKeyFromDb == null || deviceKeyFromDb.isBlank()) {
|
if (deviceKeyFromDb == null || deviceKeyFromDb.isBlank()) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
@ -315,6 +328,35 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SolanaUserPdaImportService.SessionTypeCheckResult sessionTypeCheck;
|
||||||
|
try {
|
||||||
|
sessionTypeCheck = SolanaUserPdaImportService.checkSessionTypeAgainstPda(
|
||||||
|
canonicalLogin,
|
||||||
|
sessionKey,
|
||||||
|
requestedSessionType
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Ошибка проверки sessionType по Solana PDA для login={}", canonicalLogin, e);
|
||||||
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"SESSION_TYPE_PDA_CHECK_FAILED",
|
||||||
|
"Ошибка проверки sessionType в Solana PDA"
|
||||||
|
);
|
||||||
|
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: sessionType pda check");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
if (sessionTypeCheck.hasPdaSessionRecord() && !sessionTypeCheck.matchesRequestedType()) {
|
||||||
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
AuthSessionTypeSupport.SESSION_TYPE_MISMATCH_STATUS,
|
||||||
|
"SESSION_TYPE_MISMATCH",
|
||||||
|
"sessionType не совпадает с типом сессии в Solana PDA"
|
||||||
|
);
|
||||||
|
closeConnectionAfterErrorResponse(ctx, 4001, "Auth failed: sessionType mismatch");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
// --- генерируем sessionId ---
|
// --- генерируем sessionId ---
|
||||||
String sessionId = generateRandom32B64Url();
|
String sessionId = generateRandom32B64Url();
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
@ -356,6 +398,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
clientIp,
|
clientIp,
|
||||||
clientInfoFromClient,
|
clientInfoFromClient,
|
||||||
clientInfoFromRequest,
|
clientInfoFromRequest,
|
||||||
|
requestedSessionType,
|
||||||
|
clientPlatform,
|
||||||
userLanguage
|
userLanguage
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -66,6 +66,8 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
|||||||
info.setSessionId(s.getSessionId());
|
info.setSessionId(s.getSessionId());
|
||||||
info.setClientInfoFromClient(s.getClientInfoFromClient());
|
info.setClientInfoFromClient(s.getClientInfoFromClient());
|
||||||
info.setClientInfoFromRequest(s.getClientInfoFromRequest());
|
info.setClientInfoFromRequest(s.getClientInfoFromRequest());
|
||||||
|
info.setSessionType(s.getSessionType());
|
||||||
|
info.setClientPlatform(s.getClientPlatform());
|
||||||
info.setLastAuthenticatedAtMs(s.getLastAuthirificatedAtMs());
|
info.setLastAuthenticatedAtMs(s.getLastAuthirificatedAtMs());
|
||||||
|
|
||||||
String ip = s.getClientIp();
|
String ip = s.getClientIp();
|
||||||
|
|||||||
@ -216,6 +216,16 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
|||||||
if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
|
if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
|
||||||
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
clientInfoFromClient = clientInfoFromClient.substring(0, 50);
|
||||||
}
|
}
|
||||||
|
int requestedSessionType = AuthSessionTypeSupport.normalizeRequestedSessionType(req.getSessionType());
|
||||||
|
if (!AuthSessionTypeSupport.isSupportedSessionType(requestedSessionType)) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_SESSION_TYPE",
|
||||||
|
"Неподдерживаемый sessionType"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getClientPlatform());
|
||||||
|
|
||||||
String clientIp = null;
|
String clientIp = null;
|
||||||
String clientInfoFromRequest = null;
|
String clientInfoFromRequest = null;
|
||||||
@ -235,6 +245,31 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SolanaUserPdaImportService.SessionTypeCheckResult sessionTypeCheck;
|
||||||
|
try {
|
||||||
|
sessionTypeCheck = SolanaUserPdaImportService.checkSessionTypeAgainstPda(
|
||||||
|
session.getLogin(),
|
||||||
|
sessionKeyFromReq,
|
||||||
|
requestedSessionType
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Ошибка проверки sessionType по Solana PDA для login={} sessionId={}", session.getLogin(), sessionId, e);
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"SESSION_TYPE_PDA_CHECK_FAILED",
|
||||||
|
"Ошибка проверки sessionType в Solana PDA"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sessionTypeCheck.hasPdaSessionRecord() && !sessionTypeCheck.matchesRequestedType()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
AuthSessionTypeSupport.SESSION_TYPE_MISMATCH_STATUS,
|
||||||
|
"SESSION_TYPE_MISMATCH",
|
||||||
|
"sessionType не совпадает с типом сессии в Solana PDA"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
ActiveSessionsDAO.getInstance().updateOnRefresh(
|
ActiveSessionsDAO.getInstance().updateOnRefresh(
|
||||||
@ -243,6 +278,8 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
|||||||
clientIp,
|
clientIp,
|
||||||
clientInfoFromClient,
|
clientInfoFromClient,
|
||||||
clientInfoFromRequest,
|
clientInfoFromRequest,
|
||||||
|
requestedSessionType,
|
||||||
|
clientPlatform,
|
||||||
userLanguage
|
userLanguage
|
||||||
);
|
);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@ -253,6 +290,8 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
|||||||
session.setClientIp(clientIp);
|
session.setClientIp(clientIp);
|
||||||
session.setClientInfoFromClient(clientInfoFromClient);
|
session.setClientInfoFromClient(clientInfoFromClient);
|
||||||
session.setClientInfoFromRequest(clientInfoFromRequest);
|
session.setClientInfoFromRequest(clientInfoFromRequest);
|
||||||
|
session.setSessionType(requestedSessionType);
|
||||||
|
session.setClientPlatform(clientPlatform);
|
||||||
session.setUserLanguage(userLanguage);
|
session.setUserLanguage(userLanguage);
|
||||||
|
|
||||||
// ctx
|
// ctx
|
||||||
|
|||||||
@ -14,7 +14,10 @@ import java.net.http.HttpClient;
|
|||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy-import пользователя из Solana PDA в локальную БД сервера.
|
* Lazy-import пользователя из Solana PDA в локальную БД сервера.
|
||||||
@ -57,6 +60,28 @@ public final class SolanaUserPdaImportService {
|
|||||||
return usersDao.getByLogin(login);
|
return usersDao.getByLogin(login);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SessionTypeCheckResult checkSessionTypeAgainstPda(String loginRaw, String sessionKeyApi, int requestedSessionType) throws Exception {
|
||||||
|
String login = normalizeLogin(loginRaw);
|
||||||
|
if (login == null) return SessionTypeCheckResult.noRecord();
|
||||||
|
|
||||||
|
ParsedSolanaUser parsed = fetchFromSolana(login);
|
||||||
|
if (parsed == null) return SessionTypeCheckResult.noRecord();
|
||||||
|
|
||||||
|
byte[] sessionPubKey32 = AuthSessionTypeSupport.tryParseSessionPublicKey32(sessionKeyApi);
|
||||||
|
if (sessionPubKey32 == null) return SessionTypeCheckResult.noRecord();
|
||||||
|
|
||||||
|
for (ParsedSessionRecord session : parsed.sessions()) {
|
||||||
|
if (constantTimeEquals(session.sessionPubKey32(), sessionPubKey32)) {
|
||||||
|
if (session.sessionType() == requestedSessionType) {
|
||||||
|
return SessionTypeCheckResult.match(session.sessionType(), session.sessionName());
|
||||||
|
}
|
||||||
|
return SessionTypeCheckResult.mismatch(session.sessionType(), session.sessionName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SessionTypeCheckResult.noRecord();
|
||||||
|
}
|
||||||
|
|
||||||
private static ParsedSolanaUser fetchFromSolana(String login) throws Exception {
|
private static ParsedSolanaUser fetchFromSolana(String login) throws Exception {
|
||||||
String loginB58 = toBase58(login.getBytes(StandardCharsets.UTF_8));
|
String loginB58 = toBase58(login.getBytes(StandardCharsets.UTF_8));
|
||||||
String lenB58 = toBase58(new byte[]{(byte) login.length()});
|
String lenB58 = toBase58(new byte[]{(byte) login.length()});
|
||||||
@ -135,6 +160,7 @@ public final class SolanaUserPdaImportService {
|
|||||||
byte[] blockchainKey32 = null;
|
byte[] blockchainKey32 = null;
|
||||||
byte[] deviceKey32 = null;
|
byte[] deviceKey32 = null;
|
||||||
long paidLimitBytes = 0L;
|
long paidLimitBytes = 0L;
|
||||||
|
List<ParsedSessionRecord> sessions = new ArrayList<>();
|
||||||
|
|
||||||
for (int i = 0; i < blocksCount; i++) {
|
for (int i = 0; i < blocksCount; i++) {
|
||||||
int blockType = u8(raw, c++);
|
int blockType = u8(raw, c++);
|
||||||
@ -196,11 +222,19 @@ public final class SolanaUserPdaImportService {
|
|||||||
int sessionsCount = u8(raw, c++);
|
int sessionsCount = u8(raw, c++);
|
||||||
if (sessionsCount > 64) return null;
|
if (sessionsCount > 64) return null;
|
||||||
for (int j = 0; j < sessionsCount; j++) {
|
for (int j = 0; j < sessionsCount; j++) {
|
||||||
c += 1; // session_type
|
int sessionType = u8(raw, c++);
|
||||||
c += 1; // session_version
|
int sessionVersion = u8(raw, c++);
|
||||||
int n = u8(raw, c++);
|
int n = u8(raw, c++);
|
||||||
|
String sessionName = new String(raw, c, n, StandardCharsets.UTF_8);
|
||||||
c += n;
|
c += n;
|
||||||
c += 32; // session_pub_key
|
byte[] sessionPubKey32 = slice(raw, c, 32);
|
||||||
|
c += 32;
|
||||||
|
sessions.add(new ParsedSessionRecord(
|
||||||
|
sessionType,
|
||||||
|
sessionVersion,
|
||||||
|
sessionName,
|
||||||
|
sessionPubKey32
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} else if (blockType == 70) {
|
} else if (blockType == 70) {
|
||||||
c += 1;
|
c += 1;
|
||||||
@ -217,7 +251,8 @@ public final class SolanaUserPdaImportService {
|
|||||||
blockchainName,
|
blockchainName,
|
||||||
Base64.getEncoder().encodeToString(blockchainKey32),
|
Base64.getEncoder().encodeToString(blockchainKey32),
|
||||||
Base64.getEncoder().encodeToString(deviceKey32),
|
Base64.getEncoder().encodeToString(deviceKey32),
|
||||||
paidLimitBytes
|
paidLimitBytes,
|
||||||
|
sessions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +260,7 @@ public final class SolanaUserPdaImportService {
|
|||||||
if (login == null) return null;
|
if (login == null) return null;
|
||||||
String s = login.trim();
|
String s = login.trim();
|
||||||
if (s.isEmpty()) return null;
|
if (s.isEmpty()) return null;
|
||||||
return s.toLowerCase();
|
return s.toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int u8(byte[] b, int o) { return b[o] & 0xFF; }
|
private static int u8(byte[] b, int o) { return b[o] & 0xFF; }
|
||||||
@ -272,11 +307,45 @@ public final class SolanaUserPdaImportService {
|
|||||||
return remainder;
|
return remainder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean constantTimeEquals(byte[] a, byte[] b) {
|
||||||
|
if (a == null || b == null || a.length != b.length) return false;
|
||||||
|
int diff = 0;
|
||||||
|
for (int i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
||||||
|
return diff == 0;
|
||||||
|
}
|
||||||
|
|
||||||
private record ParsedSolanaUser(
|
private record ParsedSolanaUser(
|
||||||
String login,
|
String login,
|
||||||
String blockchainName,
|
String blockchainName,
|
||||||
String blockchainKeyB64,
|
String blockchainKeyB64,
|
||||||
String deviceKeyB64,
|
String deviceKeyB64,
|
||||||
long paidLimitBytes
|
long paidLimitBytes,
|
||||||
|
List<ParsedSessionRecord> sessions
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private record ParsedSessionRecord(
|
||||||
|
int sessionType,
|
||||||
|
int sessionVersion,
|
||||||
|
String sessionName,
|
||||||
|
byte[] sessionPubKey32
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public record SessionTypeCheckResult(
|
||||||
|
boolean hasPdaSessionRecord,
|
||||||
|
boolean matchesRequestedType,
|
||||||
|
int pdaSessionType,
|
||||||
|
String sessionName
|
||||||
|
) {
|
||||||
|
static SessionTypeCheckResult noRecord() {
|
||||||
|
return new SessionTypeCheckResult(false, true, 0, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static SessionTypeCheckResult match(int pdaSessionType, String sessionName) {
|
||||||
|
return new SessionTypeCheckResult(true, true, pdaSessionType, sessionName == null ? "" : sessionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SessionTypeCheckResult mismatch(int pdaSessionType, String sessionName) {
|
||||||
|
return new SessionTypeCheckResult(true, false, pdaSessionType, sessionName == null ? "" : sessionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,12 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
|||||||
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||||
private String clientInfo;
|
private String clientInfo;
|
||||||
|
|
||||||
|
/** Числовой код типа сессии. */
|
||||||
|
private Integer sessionType;
|
||||||
|
|
||||||
|
/** Свободная строка платформы клиента, например Web / Android / ESP32. */
|
||||||
|
private String clientPlatform;
|
||||||
|
|
||||||
public String getLogin() {
|
public String getLogin() {
|
||||||
return login;
|
return login;
|
||||||
}
|
}
|
||||||
@ -104,4 +110,20 @@ public class Net_CreateAuthSession_Request extends Net_Request {
|
|||||||
public void setClientInfo(String clientInfo) {
|
public void setClientInfo(String clientInfo) {
|
||||||
this.clientInfo = clientInfo;
|
this.clientInfo = clientInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getSessionType() {
|
||||||
|
return sessionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionType(Integer sessionType) {
|
||||||
|
this.sessionType = sessionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientPlatform() {
|
||||||
|
return clientPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientPlatform(String clientPlatform) {
|
||||||
|
this.clientPlatform = clientPlatform;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,12 @@ public class Net_ListSessions_Response extends Net_Response {
|
|||||||
/** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
|
/** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
|
||||||
private String clientInfoFromRequest;
|
private String clientInfoFromRequest;
|
||||||
|
|
||||||
|
/** Числовой код типа сессии. */
|
||||||
|
private int sessionType;
|
||||||
|
|
||||||
|
/** Свободная строка платформы, как её прислал клиент. */
|
||||||
|
private String clientPlatform;
|
||||||
|
|
||||||
/** Строка геолокации вида "Country, City" или "unknown". */
|
/** Строка геолокации вида "Country, City" или "unknown". */
|
||||||
private String geo;
|
private String geo;
|
||||||
|
|
||||||
@ -84,6 +90,22 @@ public class Net_ListSessions_Response extends Net_Response {
|
|||||||
this.clientInfoFromRequest = clientInfoFromRequest;
|
this.clientInfoFromRequest = clientInfoFromRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSessionType() {
|
||||||
|
return sessionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionType(int sessionType) {
|
||||||
|
this.sessionType = sessionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientPlatform() {
|
||||||
|
return clientPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientPlatform(String clientPlatform) {
|
||||||
|
this.clientPlatform = clientPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
public String getGeo() {
|
public String getGeo() {
|
||||||
return geo;
|
return geo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,12 @@ public class Net_SessionLogin_Request extends Net_Request {
|
|||||||
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
/** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
|
||||||
private String clientInfo;
|
private String clientInfo;
|
||||||
|
|
||||||
|
/** Числовой код типа сессии. */
|
||||||
|
private Integer sessionType;
|
||||||
|
|
||||||
|
/** Свободная строка платформы клиента, например Web / Android / ESP32. */
|
||||||
|
private String clientPlatform;
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
@ -60,4 +66,20 @@ public class Net_SessionLogin_Request extends Net_Request {
|
|||||||
public void setClientInfo(String clientInfo) {
|
public void setClientInfo(String clientInfo) {
|
||||||
this.clientInfo = clientInfo;
|
this.clientInfo = clientInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getSessionType() {
|
||||||
|
return sessionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionType(Integer sessionType) {
|
||||||
|
this.sessionType = sessionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientPlatform() {
|
||||||
|
return clientPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientPlatform(String clientPlatform) {
|
||||||
|
this.clientPlatform = clientPlatform;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.179
|
client.version=1.2.180
|
||||||
server.version=1.2.168
|
server.version=1.2.169
|
||||||
|
|||||||
@ -10,6 +10,13 @@ import {
|
|||||||
|
|
||||||
export const pageMeta = { id: 'device-session-view', title: 'Сеанс устройства' };
|
export const pageMeta = { id: 'device-session-view', title: 'Сеанс устройства' };
|
||||||
|
|
||||||
|
function formatSessionType(sessionType) {
|
||||||
|
if (Number(sessionType) === 100) return 'Homeserver';
|
||||||
|
if (Number(sessionType) === 50) return 'Wallet';
|
||||||
|
if (Number(sessionType) === 1) return 'Client';
|
||||||
|
return `Type ${Number(sessionType) || 0}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatSessionTime(ms) {
|
function formatSessionTime(ms) {
|
||||||
return new Date(ms).toLocaleString('ru-RU', {
|
return new Date(ms).toLocaleString('ru-RU', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@ -46,6 +53,8 @@ export function render({ navigate, route }) {
|
|||||||
details.className = 'card stack';
|
details.className = 'card stack';
|
||||||
details.innerHTML = `
|
details.innerHTML = `
|
||||||
<div><p class="meta-muted">sessionId</p><p>${session.sessionId}</p></div>
|
<div><p class="meta-muted">sessionId</p><p>${session.sessionId}</p></div>
|
||||||
|
<div><p class="meta-muted">sessionType</p><p>${formatSessionType(session.sessionType)}</p></div>
|
||||||
|
<div><p class="meta-muted">clientPlatform</p><p>${session.clientPlatform || '-'}</p></div>
|
||||||
<div><p class="meta-muted">clientInfoFromClient</p><p>${session.clientInfoFromClient || '-'}</p></div>
|
<div><p class="meta-muted">clientInfoFromClient</p><p>${session.clientInfoFromClient || '-'}</p></div>
|
||||||
<div><p class="meta-muted">clientInfoFromRequest</p><p>${session.clientInfoFromRequest || '-'}</p></div>
|
<div><p class="meta-muted">clientInfoFromRequest</p><p>${session.clientInfoFromRequest || '-'}</p></div>
|
||||||
<div><p class="meta-muted">geo</p><p>${session.geo || 'unknown'}</p></div>
|
<div><p class="meta-muted">geo</p><p>${session.geo || 'unknown'}</p></div>
|
||||||
|
|||||||
@ -11,6 +11,13 @@ import {
|
|||||||
|
|
||||||
export const pageMeta = { id: 'device-view', title: 'Устройства' };
|
export const pageMeta = { id: 'device-view', title: 'Устройства' };
|
||||||
|
|
||||||
|
function formatSessionType(sessionType) {
|
||||||
|
if (Number(sessionType) === 100) return 'Homeserver';
|
||||||
|
if (Number(sessionType) === 50) return 'Wallet';
|
||||||
|
if (Number(sessionType) === 1) return 'Client';
|
||||||
|
return `Type ${Number(sessionType) || 0}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatSessionTime(ms) {
|
function formatSessionTime(ms) {
|
||||||
return new Date(ms).toLocaleString('ru-RU', {
|
return new Date(ms).toLocaleString('ru-RU', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@ -60,6 +67,7 @@ export function render({ navigate }) {
|
|||||||
<div class="row" style="align-items:flex-start;">
|
<div class="row" style="align-items:flex-start;">
|
||||||
<div class="stack" style="gap:4px; text-align:left;">
|
<div class="stack" style="gap:4px; text-align:left;">
|
||||||
<strong>${session.clientInfoFromClient || 'unknown client'}</strong>
|
<strong>${session.clientInfoFromClient || 'unknown client'}</strong>
|
||||||
|
<span class="meta-muted">${formatSessionType(session.sessionType)}${session.clientPlatform ? ` · ${session.clientPlatform}` : ''}</span>
|
||||||
<span class="meta-muted">${session.geo || 'unknown'}</span>
|
<span class="meta-muted">${session.geo || 'unknown'}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="meta-muted">${formatSessionTime(session.lastAuthenticatedAtMs || Date.now())}</span>
|
<span class="meta-muted">${formatSessionTime(session.lastAuthenticatedAtMs || Date.now())}</span>
|
||||||
|
|||||||
@ -53,6 +53,7 @@ const CHANNEL_TYPE_PUBLIC = 1;
|
|||||||
const CHANNEL_TYPE_PERSONAL = 100;
|
const CHANNEL_TYPE_PERSONAL = 100;
|
||||||
const CHANNEL_TYPE_GROUP = 200;
|
const CHANNEL_TYPE_GROUP = 200;
|
||||||
const CHANNEL_TYPE_VERSION_DEFAULT = 1;
|
const CHANNEL_TYPE_VERSION_DEFAULT = 1;
|
||||||
|
const SESSION_TYPE_CLIENT = 1;
|
||||||
|
|
||||||
const CONNECTION_SUBTYPES = Object.freeze({
|
const CONNECTION_SUBTYPES = Object.freeze({
|
||||||
// Legacy alias: friend == close_friend. Оба ключа ведут на один и тот же код 10/11.
|
// Legacy alias: friend == close_friend. Оба ключа ведут на один и тот же код 10/11.
|
||||||
@ -110,6 +111,10 @@ function makeClientInfo() {
|
|||||||
return ua.slice(0, 50);
|
return ua.slice(0, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeClientPlatform() {
|
||||||
|
return 'Web';
|
||||||
|
}
|
||||||
|
|
||||||
function hexToBytes(hex) {
|
function hexToBytes(hex) {
|
||||||
const clean = String(hex || '').trim().toLowerCase();
|
const clean = String(hex || '').trim().toLowerCase();
|
||||||
if (!clean || clean.length % 2 !== 0) throw new Error('Некорректный hex');
|
if (!clean || clean.length % 2 !== 0) throw new Error('Некорректный hex');
|
||||||
@ -794,6 +799,8 @@ export class AuthService {
|
|||||||
authNonce,
|
authNonce,
|
||||||
deviceKey: keyBundle.devicePair.publicKeyB64,
|
deviceKey: keyBundle.devicePair.publicKeyB64,
|
||||||
signatureB64,
|
signatureB64,
|
||||||
|
sessionType: SESSION_TYPE_CLIENT,
|
||||||
|
clientPlatform: makeClientPlatform(),
|
||||||
clientInfo: makeClientInfo(),
|
clientInfo: makeClientInfo(),
|
||||||
});
|
});
|
||||||
if (createResp.status !== 200) throw opError('CreateAuthSession', createResp);
|
if (createResp.status !== 200) throw opError('CreateAuthSession', createResp);
|
||||||
@ -932,6 +939,8 @@ export class AuthService {
|
|||||||
sessionKey: sessionMaterial.sessionKey,
|
sessionKey: sessionMaterial.sessionKey,
|
||||||
timeMs,
|
timeMs,
|
||||||
signatureB64,
|
signatureB64,
|
||||||
|
sessionType: SESSION_TYPE_CLIENT,
|
||||||
|
clientPlatform: makeClientPlatform(),
|
||||||
clientInfo: makeClientInfo(),
|
clientInfo: makeClientInfo(),
|
||||||
});
|
});
|
||||||
if (loginResp.status !== 200) throw opError('SessionLogin', loginResp);
|
if (loginResp.status !== 200) throw opError('SessionLogin', loginResp);
|
||||||
|
|||||||
@ -309,6 +309,7 @@ SessionRecord
|
|||||||
| Значение | Смысл |
|
| Значение | Смысл |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| `1` | Обычная пользовательская сессия. |
|
| `1` | Обычная пользовательская сессия. |
|
||||||
|
| `50` | Кошелёк пользователя. |
|
||||||
| `100` | Homeserver пользователя. |
|
| `100` | Homeserver пользователя. |
|
||||||
|
|
||||||
Правила:
|
Правила:
|
||||||
|
|||||||
@ -534,7 +534,7 @@ signature = Ed25519(blockchain_private_key, message_hash)
|
|||||||
|
|
||||||
- максимум `64` записей;
|
- максимум `64` записей;
|
||||||
- `sessions_mode` допускает только `1` и `10`;
|
- `sessions_mode` допускает только `1` и `10`;
|
||||||
- `session_type` допускает `1` и `100`;
|
- `session_type` допускает `1`, `50` и `100`;
|
||||||
- `session_version` сейчас только `1`;
|
- `session_version` сейчас только `1`;
|
||||||
- `session_name` должен содержать только `[A-Za-z0-9_]`;
|
- `session_name` должен содержать только `[A-Za-z0-9_]`;
|
||||||
- `session_name` и `session_pub_key` уникальны внутри списка.
|
- `session_name` и `session_pub_key` уникальны внутри списка.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user