diff --git a/Dev_Docs/API/02_Authentication_API.md b/Dev_Docs/API/02_Authentication_API.md index 73fe70f..f1b1e45 100644 --- a/Dev_Docs/API/02_Authentication_API.md +++ b/Dev_Docs/API/02_Authentication_API.md @@ -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. Поток авторизации @@ -94,6 +111,8 @@ ed25519/BASE64_PUBLIC_KEY "authNonce": "nonce", "deviceKey": "BASE64_DEVICE_PUBLIC_KEY", "signatureB64": "BASE64_SIGNATURE", + "sessionType": 1, + "clientPlatform": "Web", "clientInfo": "Android 15; Pixel 9" } } @@ -153,6 +172,8 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce} - `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`. - `422 / DEVICE_KEY_NOT_ACTUAL` — `deviceKey` не совпадает с актуальной версией на сервере. - `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` — ошибка БД при создании записи активной сессии. - `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера. @@ -208,6 +229,8 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce} "sessionKey": "ed25519/BASE64_PUBLIC_KEY", "timeMs": 1774600010456, "signatureB64": "BASE64_SIGNATURE", + "sessionType": 1, + "clientPlatform": "Web", "clientInfo": "Android 15; Pixel 9" } } @@ -258,6 +281,8 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce} - `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` не поддерживается текущим сервером. - `400 / BAD_BASE64` — неверный Base64 в `sessionKey` или `signatureB64`. - `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` — ошибка БД при чтении пользователя для этой сессии. - `422 / USER_NOT_FOUND_FOR_SESSION` — пользователь, которому принадлежит сессия, не найден. - `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера. diff --git a/Dev_Docs/API/03_Session_Management_API.md b/Dev_Docs/API/03_Session_Management_API.md index ae98b9d..d67e2ae 100644 --- a/Dev_Docs/API/03_Session_Management_API.md +++ b/Dev_Docs/API/03_Session_Management_API.md @@ -42,6 +42,8 @@ "sessions": [ { "sessionId": "sess_7c5e5c4b", + "sessionType": 1, + "clientPlatform": "Web", "clientInfoFromClient": "Android 15; Pixel 9", "clientInfoFromRequest": "UA=Java-http-client/17.0.18; remote=127.0.0.1", "geo": "RU/Moscow", @@ -58,6 +60,19 @@ - `501 / DB_ERROR_LIST_SESSIONS` — ошибка БД при чтении списка активных сессий. - `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера. +### Поля одной сессии в `ListSessions` + +- `sessionId` — идентификатор активной сессии; +- `sessionType` — числовой код типа сессии: + - `1` — клиент; + - `50` — кошелёк; + - `100` — homeserver; +- `clientPlatform` — строка платформы, как её прислал клиент; +- `clientInfoFromClient` — краткая строка клиента; +- `clientInfoFromRequest` — строка, собранная сервером из запроса; +- `geo` — страна/город или fallback-строка; +- `lastAuthenticatedAtMs` — время последней успешной авторизации этой сессии. + --- ## 2. `CloseActiveSession` diff --git a/Dev_Docs/Pending_Features/2026-06-13_2040_session_type_api_и_ui.md b/Dev_Docs/Pending_Features/2026-06-13_2040_session_type_api_и_ui.md new file mode 100644 index 0000000..93aac1c --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-13_2040_session_type_api_и_ui.md @@ -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 diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index b2b9f0b..b664bbb 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -178,6 +178,8 @@ public final class DatabaseInitializer { client_ip TEXT, client_info_from_client TEXT, client_info_from_request TEXT, + session_type INTEGER NOT NULL DEFAULT 1, + client_platform TEXT NOT NULL DEFAULT '', user_language TEXT, FOREIGN KEY (login) REFERENCES solana_users(login) ); diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/SqliteDbController.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/SqliteDbController.java index 4f3fbf2..afa321b 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/SqliteDbController.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/SqliteDbController.java @@ -14,7 +14,7 @@ import java.sql.Statement; public final class SqliteDbController { 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; @@ -86,6 +86,7 @@ public final class SqliteDbController { case 1 -> migrateToV1(); case 2 -> migrateToV2(); case 3 -> migrateToV3(); + case 4 -> migrateToV4(); 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 { st.executeUpdate(""" 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 { try (var ps = c.prepareStatement(""" INSERT INTO db_schema_version (id, schema_version, updated_at_ms) diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java index 9495f80..885dc8e 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/ActiveSessionsDAO.java @@ -47,8 +47,10 @@ public final class ActiveSessionsDAO { client_ip, client_info_from_client, client_info_from_request, + session_type, + client_platform, user_language - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """; try (PreparedStatement ps = c.prepareStatement(sql)) { @@ -64,7 +66,9 @@ public final class ActiveSessionsDAO { ps.setString(10, session.getClientIp()); ps.setString(11, session.getClientInfoFromClient()); 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(); } } @@ -92,6 +96,8 @@ public final class ActiveSessionsDAO { client_ip, client_info_from_client, client_info_from_request, + session_type, + client_platform, user_language FROM active_sessions WHERE session_id = ? @@ -127,6 +133,8 @@ public final class ActiveSessionsDAO { client_ip, client_info_from_client, client_info_from_request, + session_type, + client_platform, user_language FROM active_sessions WHERE login = ? COLLATE NOCASE @@ -179,6 +187,8 @@ public final class ActiveSessionsDAO { String clientIp, String clientInfoFromClient, String clientInfoFromRequest, + int sessionType, + String clientPlatform, String userLanguage ) throws SQLException { @@ -189,6 +199,8 @@ public final class ActiveSessionsDAO { client_ip = ?, client_info_from_client = ?, client_info_from_request = ?, + session_type = ?, + client_platform = ?, user_language = ? WHERE session_id = ? """; @@ -198,8 +210,10 @@ public final class ActiveSessionsDAO { ps.setString(2, clientIp); ps.setString(3, clientInfoFromClient); ps.setString(4, clientInfoFromRequest); - ps.setString(5, userLanguage); - ps.setString(6, sessionId); + ps.setInt(5, sessionType); + ps.setString(6, clientPlatform); + ps.setString(7, userLanguage); + ps.setString(8, sessionId); ps.executeUpdate(); } } @@ -210,10 +224,12 @@ public final class ActiveSessionsDAO { String clientIp, String clientInfoFromClient, String clientInfoFromRequest, + int sessionType, + String clientPlatform, String userLanguage ) throws SQLException { 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 clientInfoFromClient = rs.getString("client_info_from_client"); 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"); return new ActiveSessionEntry( @@ -283,6 +301,8 @@ public final class ActiveSessionsDAO { clientIp, clientInfoFromClient, clientInfoFromRequest, + sessionType, + clientPlatform, userLanguage ); } diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/ActiveSessionEntry.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/ActiveSessionEntry.java index 2a2bb07..9c40c60 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/ActiveSessionEntry.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/entities/ActiveSessionEntry.java @@ -22,6 +22,8 @@ public class ActiveSessionEntry { private String clientIp; private String clientInfoFromClient; private String clientInfoFromRequest; + private int sessionType; + private String clientPlatform; private String userLanguage; public ActiveSessionEntry() { } @@ -38,6 +40,8 @@ public class ActiveSessionEntry { String clientIp, String clientInfoFromClient, String clientInfoFromRequest, + int sessionType, + String clientPlatform, String userLanguage) { this.sessionId = sessionId; this.login = login; @@ -51,6 +55,8 @@ public class ActiveSessionEntry { this.clientIp = clientIp; this.clientInfoFromClient = clientInfoFromClient; this.clientInfoFromRequest = clientInfoFromRequest; + this.sessionType = sessionType; + this.clientPlatform = clientPlatform; this.userLanguage = userLanguage; } @@ -90,6 +96,12 @@ public class ActiveSessionEntry { public String getClientInfoFromRequest() { return 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 void setUserLanguage(String userLanguage) { this.userLanguage = userLanguage; } } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/AuthSessionTypeSupport.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/AuthSessionTypeSupport.java new file mode 100644 index 0000000..f5fbb28 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/AuthSessionTypeSupport.java @@ -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; + } + } + } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java index fb6759c..be89190 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java @@ -213,6 +213,19 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { 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(); if (deviceKeyFromDb == null || deviceKeyFromDb.isBlank()) { Net_Response err = NetExceptionResponseFactory.error( @@ -315,6 +328,35 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { 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 --- String sessionId = generateRandom32B64Url(); long now = System.currentTimeMillis(); @@ -356,6 +398,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { clientIp, clientInfoFromClient, clientInfoFromRequest, + requestedSessionType, + clientPlatform, userLanguage ); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java index 50e73b9..6c47505 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ListSessions_Handler.java @@ -66,6 +66,8 @@ public class Net_ListSessions_Handler implements JsonMessageHandler { info.setSessionId(s.getSessionId()); info.setClientInfoFromClient(s.getClientInfoFromClient()); info.setClientInfoFromRequest(s.getClientInfoFromRequest()); + info.setSessionType(s.getSessionType()); + info.setClientPlatform(s.getClientPlatform()); info.setLastAuthenticatedAtMs(s.getLastAuthirificatedAtMs()); String ip = s.getClientIp(); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java index 8adac28..7859c3e 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java @@ -216,6 +216,16 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler { if (clientInfoFromClient != null && clientInfoFromClient.length() > 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 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(); try { ActiveSessionsDAO.getInstance().updateOnRefresh( @@ -243,6 +278,8 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler { clientIp, clientInfoFromClient, clientInfoFromRequest, + requestedSessionType, + clientPlatform, userLanguage ); } catch (SQLException e) { @@ -253,6 +290,8 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler { session.setClientIp(clientIp); session.setClientInfoFromClient(clientInfoFromClient); session.setClientInfoFromRequest(clientInfoFromRequest); + session.setSessionType(requestedSessionType); + session.setClientPlatform(clientPlatform); session.setUserLanguage(userLanguage); // ctx diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java index 3183fbe..6b76347 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java @@ -14,7 +14,10 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; +import java.util.Locale; /** * Lazy-import пользователя из Solana PDA в локальную БД сервера. @@ -57,6 +60,28 @@ public final class SolanaUserPdaImportService { 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 { String loginB58 = toBase58(login.getBytes(StandardCharsets.UTF_8)); String lenB58 = toBase58(new byte[]{(byte) login.length()}); @@ -135,6 +160,7 @@ public final class SolanaUserPdaImportService { byte[] blockchainKey32 = null; byte[] deviceKey32 = null; long paidLimitBytes = 0L; + List sessions = new ArrayList<>(); for (int i = 0; i < blocksCount; i++) { int blockType = u8(raw, c++); @@ -196,11 +222,19 @@ public final class SolanaUserPdaImportService { int sessionsCount = u8(raw, c++); if (sessionsCount > 64) return null; for (int j = 0; j < sessionsCount; j++) { - c += 1; // session_type - c += 1; // session_version + int sessionType = u8(raw, c++); + int sessionVersion = u8(raw, c++); int n = u8(raw, c++); + String sessionName = new String(raw, c, n, StandardCharsets.UTF_8); 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) { c += 1; @@ -217,7 +251,8 @@ public final class SolanaUserPdaImportService { blockchainName, Base64.getEncoder().encodeToString(blockchainKey32), Base64.getEncoder().encodeToString(deviceKey32), - paidLimitBytes + paidLimitBytes, + sessions ); } @@ -225,7 +260,7 @@ public final class SolanaUserPdaImportService { if (login == null) return null; String s = login.trim(); 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; } @@ -272,11 +307,45 @@ public final class SolanaUserPdaImportService { 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( String login, String blockchainName, String blockchainKeyB64, String deviceKeyB64, - long paidLimitBytes + long paidLimitBytes, + List 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); + } + } } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Request.java index cda4f80..91fa95b 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Request.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Request.java @@ -41,6 +41,12 @@ public class Net_CreateAuthSession_Request extends Net_Request { /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */ private String clientInfo; + /** Числовой код типа сессии. */ + private Integer sessionType; + + /** Свободная строка платформы клиента, например Web / Android / ESP32. */ + private String clientPlatform; + public String getLogin() { return login; } @@ -104,4 +110,20 @@ public class Net_CreateAuthSession_Request extends Net_Request { public void setClientInfo(String 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; + } } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ListSessions_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ListSessions_Response.java index 08219d1..2bd3571 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ListSessions_Response.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ListSessions_Response.java @@ -52,6 +52,12 @@ public class Net_ListSessions_Response extends Net_Response { /** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */ private String clientInfoFromRequest; + /** Числовой код типа сессии. */ + private int sessionType; + + /** Свободная строка платформы, как её прислал клиент. */ + private String clientPlatform; + /** Строка геолокации вида "Country, City" или "unknown". */ private String geo; @@ -84,6 +90,22 @@ public class Net_ListSessions_Response extends Net_Response { 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() { return geo; } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_SessionLogin_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_SessionLogin_Request.java index e1f1d4d..51483f9 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_SessionLogin_Request.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_SessionLogin_Request.java @@ -21,6 +21,12 @@ public class Net_SessionLogin_Request extends Net_Request { /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */ private String clientInfo; + /** Числовой код типа сессии. */ + private Integer sessionType; + + /** Свободная строка платформы клиента, например Web / Android / ESP32. */ + private String clientPlatform; + public String getSessionId() { return sessionId; } @@ -60,4 +66,20 @@ public class Net_SessionLogin_Request extends Net_Request { public void setClientInfo(String 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; + } } diff --git a/VERSION.properties b/VERSION.properties index 523bb02..f980545 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.179 -server.version=1.2.168 +client.version=1.2.180 +server.version=1.2.169 diff --git a/shine-UI/js/pages/device-session-view.js b/shine-UI/js/pages/device-session-view.js index 2f01aa6..c8cb4df 100644 --- a/shine-UI/js/pages/device-session-view.js +++ b/shine-UI/js/pages/device-session-view.js @@ -10,6 +10,13 @@ import { 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) { return new Date(ms).toLocaleString('ru-RU', { day: '2-digit', @@ -46,6 +53,8 @@ export function render({ navigate, route }) { details.className = 'card stack'; details.innerHTML = `

sessionId

${session.sessionId}

+

sessionType

${formatSessionType(session.sessionType)}

+

clientPlatform

${session.clientPlatform || '-'}

clientInfoFromClient

${session.clientInfoFromClient || '-'}

clientInfoFromRequest

${session.clientInfoFromRequest || '-'}

geo

${session.geo || 'unknown'}

diff --git a/shine-UI/js/pages/device-view.js b/shine-UI/js/pages/device-view.js index 9360e3e..7f5b60d 100644 --- a/shine-UI/js/pages/device-view.js +++ b/shine-UI/js/pages/device-view.js @@ -11,6 +11,13 @@ import { 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) { return new Date(ms).toLocaleString('ru-RU', { day: '2-digit', @@ -60,6 +67,7 @@ export function render({ navigate }) {
${session.clientInfoFromClient || 'unknown client'} + ${formatSessionType(session.sessionType)}${session.clientPlatform ? ` · ${session.clientPlatform}` : ''} ${session.geo || 'unknown'}
${formatSessionTime(session.lastAuthenticatedAtMs || Date.now())} diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 9ba1891..e370da6 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -53,6 +53,7 @@ const CHANNEL_TYPE_PUBLIC = 1; const CHANNEL_TYPE_PERSONAL = 100; const CHANNEL_TYPE_GROUP = 200; const CHANNEL_TYPE_VERSION_DEFAULT = 1; +const SESSION_TYPE_CLIENT = 1; const CONNECTION_SUBTYPES = Object.freeze({ // Legacy alias: friend == close_friend. Оба ключа ведут на один и тот же код 10/11. @@ -110,6 +111,10 @@ function makeClientInfo() { return ua.slice(0, 50); } +function makeClientPlatform() { + return 'Web'; +} + function hexToBytes(hex) { const clean = String(hex || '').trim().toLowerCase(); if (!clean || clean.length % 2 !== 0) throw new Error('Некорректный hex'); @@ -794,6 +799,8 @@ export class AuthService { authNonce, deviceKey: keyBundle.devicePair.publicKeyB64, signatureB64, + sessionType: SESSION_TYPE_CLIENT, + clientPlatform: makeClientPlatform(), clientInfo: makeClientInfo(), }); if (createResp.status !== 200) throw opError('CreateAuthSession', createResp); @@ -932,6 +939,8 @@ export class AuthService { sessionKey: sessionMaterial.sessionKey, timeMs, signatureB64, + sessionType: SESSION_TYPE_CLIENT, + clientPlatform: makeClientPlatform(), clientInfo: makeClientInfo(), }); if (loginResp.status !== 200) throw opError('SessionLogin', loginResp); diff --git a/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md b/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md index 1039e38..556e0c4 100644 --- a/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md +++ b/shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md @@ -309,6 +309,7 @@ SessionRecord | Значение | Смысл | |----------|-------| | `1` | Обычная пользовательская сессия. | +| `50` | Кошелёк пользователя. | | `100` | Homeserver пользователя. | Правила: diff --git a/shine-solana/shine/doc/programs/shine_users.md b/shine-solana/shine/doc/programs/shine_users.md index 7f01b48..e53e43d 100644 --- a/shine-solana/shine/doc/programs/shine_users.md +++ b/shine-solana/shine/doc/programs/shine_users.md @@ -534,7 +534,7 @@ signature = Ed25519(blockchain_private_key, message_hash) - максимум `64` записей; - `sessions_mode` допускает только `1` и `10`; -- `session_type` допускает `1` и `100`; +- `session_type` допускает `1`, `50` и `100`; - `session_version` сейчас только `1`; - `session_name` должен содержать только `[A-Za-z0-9_]`; - `session_name` и `session_pub_key` уникальны внутри списка.