diff --git a/Dev_Docs/00_INDEX.md b/Dev_Docs/00_INDEX.md index 1d6d246..ecc1f8d 100644 --- a/Dev_Docs/00_INDEX.md +++ b/Dev_Docs/00_INDEX.md @@ -16,6 +16,9 @@ 0. **API/03_Session_Management_API.md** Глава API по управлению сессиями: `ListSessions` и `CloseActiveSession`. +0. **API/05_Technical_Requests_API.md** + Технические запросы без авторизации: `Ping` для keep-alive и `GetServerInfo` для проверки доступности узла и чтения его публичной информации. + 1. **01_Connection_and_Sessions.md** Процесс подключения к WebSocket, авторизация (двухшаговая), создание сессии, вход в существующую сессию, просмотр и закрытие сессий. diff --git a/Dev_Docs/API/05_Technical_Requests_API.md b/Dev_Docs/API/05_Technical_Requests_API.md new file mode 100644 index 0000000..b96f972 --- /dev/null +++ b/Dev_Docs/API/05_Technical_Requests_API.md @@ -0,0 +1,137 @@ +# API для разработчиков: Технические запросы + +Этот файл описывает технические запросы, которые не требуют авторизации и нужны для служебной работы клиента с сервером. + +Сейчас здесь два метода: + +- `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения; +- `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети. + +Логика раздела такая: + +- `Ping` нужен для регулярной проверки, что соединение всё ещё живо; +- `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла. + +Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов. + +## 1. `Ping` + +### Назначение + +Служебный keep-alive запрос. + +Клиент может отправлять его периодически, чтобы: + +- поддерживать активное WebSocket-соединение; +- понимать, что сервер отвечает; +- при необходимости получать текущее серверное время. + +### Запрос + +```json +{ + "op": "Ping", + "requestId": "ping-001", + "payload": { + "ts": 1774700000123 + } +} +``` + +Поле `ts` в запросе необязательно для логики сервера. Сервер его не валидирует и не использует для принятия решения. + +### Успешный ответ + +```json +{ + "op": "Ping", + "requestId": "ping-001", + "status": 200, + "ok": true, + "payload": { + "ts": 1774700000456 + } +} +``` + +### Специфические коды ошибок `Ping` + +- У `Ping` нет специальных прикладных ошибок. +- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`. + +--- + +## 2. `GetServerInfo` + +### Назначение + +Запрос публичной информации о сервере. + +Он нужен клиенту для выбора сервера в децентрализованной сети. По этому запросу клиент может: + +- проверить, что сервер вообще доступен; +- показать URL и версию сервера; +- показать физический регион или адрес размещения; +- показать описание сервера; +- показать поле `origin` как комментарий о природе этого узла; +- показать дополнительную текстовую информацию. + +Этот запрос доступен без авторизации. + +### Источник данных + +- `version` берётся из Gradle build и подставляется в `application.properties`; +- остальные поля читаются из настроек сервера; +- если значение в конфиге не задано, сервер возвращает пустую строку. + +### Запрос + +```json +{ + "op": "GetServerInfo", + "requestId": "srv-001", + "payload": { + } +} +``` + +### Успешный ответ + +```json +{ + "op": "GetServerInfo", + "requestId": "srv-001", + "status": 200, + "ok": true, + "payload": { + "url": "wss://node.example.org/ws", + "version": "1.0", + "physicalRegion": "Грузия, Тбилиси", + "description": "Public community SHiNE node", + "origin": "Community-operated node", + "extraInfo": "IPv4 + IPv6; test federation enabled" + } +} +``` + +### Поля ответа + +- `url` — публичный URL сервера. +- `version` — версия сервера из Gradle build. +- `physicalRegion` — физический регион или адрес размещения сервера. +- `description` — человекочитаемое описание сервера. +- `origin` — комментарий о том, какой это сервер. +- `extraInfo` — любая дополнительная информация о сервере. + +### Специфические коды ошибок `GetServerInfo` + +- У `GetServerInfo` нет специальных прикладных ошибок при штатной работе. +- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`. + +--- + +## 3. Короткое резюме + +- `Ping` нужен для keep-alive и проверки, что WebSocket-соединение живо. +- `GetServerInfo` нужен для выбора сервера в сети и показа публичной информации об узле. +- Оба запроса доступны без авторизации. diff --git a/build.gradle b/build.gradle index 3c27065..ea9d854 100644 --- a/build.gradle +++ b/build.gradle @@ -5,12 +5,19 @@ plugins { } group = 'shine' -version = '1.0' +version = '1.1_codex' tasks.jar { enabled = false // это что бы не создавала обычный джар, а будет только толстый джар } +tasks.processResources { + filteringCharset = 'UTF-8' + filesMatching('application.properties') { + expand(projectVersion: project.version.toString()) + } +} + repositories { mavenCentral() } @@ -142,4 +149,3 @@ tasks.register('itDeployServer', JavaExec) { dependsOn testClasses } - diff --git a/shine-server-config/src/main/java/utils/config/AppConfig.java b/shine-server-config/src/main/java/utils/config/AppConfig.java index 934afbf..5cdde05 100644 --- a/shine-server-config/src/main/java/utils/config/AppConfig.java +++ b/shine-server-config/src/main/java/utils/config/AppConfig.java @@ -45,6 +45,12 @@ public final class AppConfig { return properties.getProperty(name); } + /** Вернёт строку или пустую строку, если параметр не найден. */ + public String getStringOrEmpty(String name) { + String value = properties.getProperty(name); + return value == null ? "" : value.trim(); + } + /** Можно добавить методы для удобства */ public int getInt(String name, int defaultValue) { String v = properties.getProperty(name); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index 386e85e..4f608cb 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -47,7 +47,9 @@ import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Ha import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request; // --- NEW: Ping --- +import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler; +import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request; import java.util.Map; @@ -85,7 +87,8 @@ public final class JsonHandlerRegistry { Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()), // --- system --- - Map.entry("Ping", new Net_Ping_Handler()) + Map.entry("Ping", new Net_Ping_Handler()), + Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()) // --- subscriptions --- // Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler()) @@ -118,7 +121,8 @@ public final class JsonHandlerRegistry { Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class), // --- system --- - Map.entry("Ping", Net_Ping_Request.class) + Map.entry("Ping", Net_Ping_Request.class), + Map.entry("GetServerInfo", Net_GetServerInfo_Request.class) ); private JsonHandlerRegistry() { } @@ -130,4 +134,4 @@ public final class JsonHandlerRegistry { public static Map> getRequestTypes() { return REQUEST_TYPES; } -} \ No newline at end of file +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d22a61d..0173e02 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,14 @@ server.1port=7070 db.path=data/shine.sqlite +# ------------------------------------------------------------ +# Server public info +# Эти поля используются JSON-операцией GetServerInfo. +# Если какое-то значение не задано, сервер вернёт пустую строку. +# ------------------------------------------------------------ +server.version=${projectVersion} +server.info.url= +server.info.physicalRegion= +server.info.description= +server.info.origin= +server.info.extraInfo= diff --git a/src/test/java/test/it/cases/IT_01_AddUser.java b/src/test/java/test/it/cases/IT_01_AddUser.java index d05335e..f621348 100644 --- a/src/test/java/test/it/cases/IT_01_AddUser.java +++ b/src/test/java/test/it/cases/IT_01_AddUser.java @@ -36,6 +36,8 @@ public class IT_01_AddUser { try (WsSession ws = WsSession.open()) { + checkPingAndServerInfo(r, ws, t); + r.ok("AddUser USER1: " + TestConfig.LOGIN()); String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t); checkAddUser200or409(r, resp1); @@ -76,6 +78,38 @@ public class IT_01_AddUser { return r.summaryLine(); } + private static void checkPingAndServerInfo(TestResult r, WsSession ws, Duration t) { + String pingResp = ws.call("Ping", JsonBuilders.ping(System.currentTimeMillis()), t); + if (JsonParsers.status(pingResp) != 200 || !Boolean.TRUE.equals(JsonParsers.ok(pingResp))) { + r.fail("Ping: ожидали status=200 и ok=true, resp=" + pingResp); + fail("Ping unexpected response"); + } + + Long serverTs = JsonParsers.pingTs(pingResp); + if (serverTs == null || serverTs <= 0) { + r.fail("Ping: сервер не вернул ts, resp=" + pingResp); + fail("Ping missing ts"); + } + r.ok("Ping: ok, serverTs=" + serverTs); + + String infoResp = ws.call("GetServerInfo", JsonBuilders.getServerInfo(), t); + if (JsonParsers.status(infoResp) != 200 || !Boolean.TRUE.equals(JsonParsers.ok(infoResp))) { + r.fail("GetServerInfo: ожидали status=200 и ok=true, resp=" + infoResp); + fail("GetServerInfo unexpected response"); + } + if (!JsonParsers.payloadIsObject(infoResp)) { + r.fail("GetServerInfo: payload должен быть объектом, resp=" + infoResp); + fail("GetServerInfo payload is not object"); + } + + r.ok("GetServerInfo: ok, url='" + safe(JsonParsers.payloadText(infoResp, "url")) + + "', version='" + safe(JsonParsers.payloadText(infoResp, "version")) + + "', physicalRegion='" + safe(JsonParsers.payloadText(infoResp, "physicalRegion")) + + "', description='" + safe(JsonParsers.payloadText(infoResp, "description")) + + "', origin='" + safe(JsonParsers.payloadText(infoResp, "origin")) + + "', extraInfo='" + safe(JsonParsers.payloadText(infoResp, "extraInfo")) + "'"); + } + private static void checkAddUser200or409(TestResult r, String resp) { int st = JsonParsers.status(resp); if (st == 200) { @@ -324,4 +358,8 @@ public class IT_01_AddUser { private static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); } + + private static String safe(String s) { + return s == null ? "" : s; + } } diff --git a/src/test/java/test/it/runner/IT_RunAllMain.java b/src/test/java/test/it/runner/IT_RunAllMain.java index e6d97b1..7ae6209 100644 --- a/src/test/java/test/it/runner/IT_RunAllMain.java +++ b/src/test/java/test/it/runner/IT_RunAllMain.java @@ -1,6 +1,7 @@ package test.it.runner; import test.it.cases.IT_01_AddUser; +import test.it.cases.IT_00_TechnicalRequests; import test.it.cases.IT_02_Sessions; import test.it.cases.IT_03_AddBlock_NoAuth; import test.it.cases.IT_04_UserParams_NoAuth; @@ -37,6 +38,9 @@ public class IT_RunAllMain { TestLog.title("IT RUN: запуск всех тестов подряд" + (STOP_ON_FIRST_FAIL ? " (STOP_ON_FIRST_FAIL=ON)" : " (STOP_ON_FIRST_FAIL=OFF)")); + String s0 = IT_00_TechnicalRequests.run(); summaries.add(s0); + if (s0.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); } + String s1 = IT_01_AddUser.run(); summaries.add(s1); if (s1.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); } @@ -69,4 +73,4 @@ public class IT_RunAllMain { return failed; } -} \ No newline at end of file +} diff --git a/src/test/java/test/it/utils/json/JsonBuilders.java b/src/test/java/test/it/utils/json/JsonBuilders.java index bda46b0..dd5d1fe 100644 --- a/src/test/java/test/it/utils/json/JsonBuilders.java +++ b/src/test/java/test/it/utils/json/JsonBuilders.java @@ -90,6 +90,35 @@ public final class JsonBuilders { """.formatted(requestId, login); } + // ---------------- Ping ---------------- + + public static String ping(long ts) { + String requestId = TestIds.next("ping"); + return """ + { + "op": "Ping", + "requestId": "%s", + "payload": { + "ts": %d + } + } + """.formatted(requestId, ts); + } + + // ---------------- GetServerInfo ---------------- + + public static String getServerInfo() { + String requestId = TestIds.next("serverinfo"); + return """ + { + "op": "GetServerInfo", + "requestId": "%s", + "payload": { + } + } + """.formatted(requestId); + } + // ---------------- AuthChallenge ---------------- public static String authChallenge(String login) { diff --git a/src/test/java/test/it/utils/json/JsonParsers.java b/src/test/java/test/it/utils/json/JsonParsers.java index ca10cbc..ae76e43 100644 --- a/src/test/java/test/it/utils/json/JsonParsers.java +++ b/src/test/java/test/it/utils/json/JsonParsers.java @@ -132,6 +132,21 @@ public final class JsonParsers { } } + public static Long pingTs(String json) { + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("ts")) return payload.get("ts").asLong(); + return null; + } catch (Exception e) { + return null; + } + } + + public static String payloadText(String json, String field) { + return getPayloadText(json, field); + } + public static List sessionIds(String json) { List res = new ArrayList<>(); try {