30 03 25
Добавил АПИ функцию которая возвращает информацию о версии сервера и о том что он работает
This commit is contained in:
parent
1aabcf4d80
commit
99cf000f24
@ -16,6 +16,9 @@
|
|||||||
0. **API/03_Session_Management_API.md**
|
0. **API/03_Session_Management_API.md**
|
||||||
Глава API по управлению сессиями: `ListSessions` и `CloseActiveSession`.
|
Глава API по управлению сессиями: `ListSessions` и `CloseActiveSession`.
|
||||||
|
|
||||||
|
0. **API/05_Technical_Requests_API.md**
|
||||||
|
Технические запросы без авторизации: `Ping` для keep-alive и `GetServerInfo` для проверки доступности узла и чтения его публичной информации.
|
||||||
|
|
||||||
1. **01_Connection_and_Sessions.md**
|
1. **01_Connection_and_Sessions.md**
|
||||||
Процесс подключения к WebSocket, авторизация (двухшаговая), создание сессии, вход в существующую сессию, просмотр и закрытие сессий.
|
Процесс подключения к WebSocket, авторизация (двухшаговая), создание сессии, вход в существующую сессию, просмотр и закрытие сессий.
|
||||||
|
|
||||||
|
|||||||
137
Dev_Docs/API/05_Technical_Requests_API.md
Normal file
137
Dev_Docs/API/05_Technical_Requests_API.md
Normal file
@ -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` нужен для выбора сервера в сети и показа публичной информации об узле.
|
||||||
|
- Оба запроса доступны без авторизации.
|
||||||
10
build.gradle
10
build.gradle
@ -5,12 +5,19 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = 'shine'
|
group = 'shine'
|
||||||
version = '1.0'
|
version = '1.1_codex'
|
||||||
|
|
||||||
tasks.jar {
|
tasks.jar {
|
||||||
enabled = false // это что бы не создавала обычный джар, а будет только толстый джар
|
enabled = false // это что бы не создавала обычный джар, а будет только толстый джар
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
filteringCharset = 'UTF-8'
|
||||||
|
filesMatching('application.properties') {
|
||||||
|
expand(projectVersion: project.version.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -142,4 +149,3 @@ tasks.register('itDeployServer', JavaExec) {
|
|||||||
|
|
||||||
dependsOn testClasses
|
dependsOn testClasses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,12 @@ public final class AppConfig {
|
|||||||
return properties.getProperty(name);
|
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) {
|
public int getInt(String name, int defaultValue) {
|
||||||
String v = properties.getProperty(name);
|
String v = properties.getProperty(name);
|
||||||
|
|||||||
@ -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;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
|
||||||
|
|
||||||
// --- NEW: Ping ---
|
// --- 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.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 server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -85,7 +87,8 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
||||||
|
|
||||||
// --- system ---
|
// --- system ---
|
||||||
Map.entry("Ping", new Net_Ping_Handler())
|
Map.entry("Ping", new Net_Ping_Handler()),
|
||||||
|
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler())
|
||||||
|
|
||||||
// --- subscriptions ---
|
// --- subscriptions ---
|
||||||
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
||||||
@ -118,7 +121,8 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
||||||
|
|
||||||
// --- system ---
|
// --- system ---
|
||||||
Map.entry("Ping", Net_Ping_Request.class)
|
Map.entry("Ping", Net_Ping_Request.class),
|
||||||
|
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class)
|
||||||
);
|
);
|
||||||
|
|
||||||
private JsonHandlerRegistry() { }
|
private JsonHandlerRegistry() { }
|
||||||
|
|||||||
@ -1,3 +1,14 @@
|
|||||||
server.1port=7070
|
server.1port=7070
|
||||||
db.path=data/shine.sqlite
|
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=
|
||||||
|
|||||||
@ -36,6 +36,8 @@ public class IT_01_AddUser {
|
|||||||
|
|
||||||
try (WsSession ws = WsSession.open()) {
|
try (WsSession ws = WsSession.open()) {
|
||||||
|
|
||||||
|
checkPingAndServerInfo(r, ws, t);
|
||||||
|
|
||||||
r.ok("AddUser USER1: " + TestConfig.LOGIN());
|
r.ok("AddUser USER1: " + TestConfig.LOGIN());
|
||||||
String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t);
|
String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t);
|
||||||
checkAddUser200or409(r, resp1);
|
checkAddUser200or409(r, resp1);
|
||||||
@ -76,6 +78,38 @@ public class IT_01_AddUser {
|
|||||||
return r.summaryLine();
|
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) {
|
private static void checkAddUser200or409(TestResult r, String resp) {
|
||||||
int st = JsonParsers.status(resp);
|
int st = JsonParsers.status(resp);
|
||||||
if (st == 200) {
|
if (st == 200) {
|
||||||
@ -324,4 +358,8 @@ public class IT_01_AddUser {
|
|||||||
private static boolean isBlank(String s) {
|
private static boolean isBlank(String s) {
|
||||||
return s == null || s.trim().isEmpty();
|
return s == null || s.trim().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String safe(String s) {
|
||||||
|
return s == null ? "" : s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package test.it.runner;
|
package test.it.runner;
|
||||||
|
|
||||||
import test.it.cases.IT_01_AddUser;
|
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_02_Sessions;
|
||||||
import test.it.cases.IT_03_AddBlock_NoAuth;
|
import test.it.cases.IT_03_AddBlock_NoAuth;
|
||||||
import test.it.cases.IT_04_UserParams_NoAuth;
|
import test.it.cases.IT_04_UserParams_NoAuth;
|
||||||
@ -37,6 +38,9 @@ public class IT_RunAllMain {
|
|||||||
TestLog.title("IT RUN: запуск всех тестов подряд"
|
TestLog.title("IT RUN: запуск всех тестов подряд"
|
||||||
+ (STOP_ON_FIRST_FAIL ? " (STOP_ON_FIRST_FAIL=ON)" : " (STOP_ON_FIRST_FAIL=OFF)"));
|
+ (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);
|
String s1 = IT_01_AddUser.run(); summaries.add(s1);
|
||||||
if (s1.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
|
if (s1.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
|
||||||
|
|
||||||
|
|||||||
@ -90,6 +90,35 @@ public final class JsonBuilders {
|
|||||||
""".formatted(requestId, login);
|
""".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 ----------------
|
// ---------------- AuthChallenge ----------------
|
||||||
|
|
||||||
public static String authChallenge(String login) {
|
public static String authChallenge(String login) {
|
||||||
|
|||||||
@ -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<String> sessionIds(String json) {
|
public static List<String> sessionIds(String json) {
|
||||||
List<String> res = new ArrayList<>();
|
List<String> res = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user