11 12 25
Добавил Закрытие сессии
This commit is contained in:
parent
80ffba545a
commit
a6be7b75aa
@ -125,7 +125,7 @@ public final class ActiveSessionsDAO {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновить только lastAuthirificatedAtMs для конкретной сессии.
|
* Обновить только lastAuthirificatedAtMs для конкретной сессии.
|
||||||
* (оставляю для совместимости, вдруг ещё где-то используется)
|
* (оставлено для совместимости)
|
||||||
*/
|
*/
|
||||||
public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
|
public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
|
|||||||
@ -12,7 +12,7 @@ public class ConnectionContext {
|
|||||||
|
|
||||||
// Статусы аутентификации
|
// Статусы аутентификации
|
||||||
public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
|
public static final int AUTH_STATUS_NONE = 0; // анонимный / не авторизован
|
||||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // получен AuthChallenge, ждём CreateAuthSession
|
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // получен AuthChallenge
|
||||||
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
||||||
|
|
||||||
// Полный пользователь из БД (solana_users)
|
// Полный пользователь из БД (solana_users)
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
package server.logic.ws_protocol.JSON;
|
package server.logic.ws_protocol.JSON;
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.*;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_AuthChallenge_Request;
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_AuthChallenge_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Request;
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_RefreshSession_Request;
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_RefreshSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.*;
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Request;
|
import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -25,18 +27,20 @@ import java.util.Map;
|
|||||||
public final class JsonHandlerRegistry {
|
public final class JsonHandlerRegistry {
|
||||||
|
|
||||||
private static final Map<String, JsonMessageHandler> HANDLERS = Map.of(
|
private static final Map<String, JsonMessageHandler> HANDLERS = Map.of(
|
||||||
"RefreshSession", new Net_RefreshSession_Handler(),
|
"RefreshSession", new Net_RefreshSession_Handler(),
|
||||||
"AddUser", new Net_AddUser_Handler(),
|
"AddUser", new Net_AddUser_Handler(),
|
||||||
"AuthChallenge", new Net_AuthChallenge_Handler(),
|
"AuthChallenge", new Net_AuthChallenge_Handler(),
|
||||||
"CreateAuthSession", new Net_CreateAuthSession__Handler()
|
"CreateAuthSession", new Net_CreateAuthSession__Handler(),
|
||||||
|
"CloseActiveSession", new Net_CloseActiveSession_Handler()
|
||||||
// сюда потом добавишь другие операции
|
// сюда потом добавишь другие операции
|
||||||
);
|
);
|
||||||
|
|
||||||
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.of(
|
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.of(
|
||||||
"RefreshSession", Net_RefreshSession_Request.class,
|
"RefreshSession", Net_RefreshSession_Request.class,
|
||||||
"AddUser", Net_AddUser_Request.class,
|
"AddUser", Net_AddUser_Request.class,
|
||||||
"AuthChallenge", Net_AuthChallenge_Request.class,
|
"AuthChallenge", Net_AuthChallenge_Request.class,
|
||||||
"CreateAuthSession", Net_CreateAuthSession_Request.class
|
"CreateAuthSession", Net_CreateAuthSession_Request.class,
|
||||||
|
"CloseActiveSession", Net_CloseActiveSession_Request.class
|
||||||
);
|
);
|
||||||
|
|
||||||
private JsonHandlerRegistry() {
|
private JsonHandlerRegistry() {
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.entyties.Auth;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запрос CloseActiveSession — закрытие активной сессии пользователя.
|
||||||
|
*
|
||||||
|
* Допустимые режимы:
|
||||||
|
*
|
||||||
|
* 1) Пользователь уже авторизован (AUTH_STATUS_USER):
|
||||||
|
* - поле sessionId:
|
||||||
|
* * если заполнено — закрывается указанная сессия пользователя;
|
||||||
|
* * если пустое — закрывается текущая авторизованная сессия
|
||||||
|
* (та, в рамках которой выполняется запрос).
|
||||||
|
* - поля timeMs и signatureB64 могут быть пустыми и игнорируются.
|
||||||
|
*
|
||||||
|
* 2) Пользователь в статусе AUTH_STATUS_AUTH_IN_PROGRESS:
|
||||||
|
* - требуется дополнительно подтвердить владение ключом:
|
||||||
|
* * timeMs — время на клиенте (мс с 1970-01-01),
|
||||||
|
* * signatureB64 — подпись Ed25519 над строкой
|
||||||
|
* "AUTHORIFICATED:" + timeMs + authNonce.
|
||||||
|
* - authNonce берётся из шага 1 (AuthChallenge) и хранится в ctx.authNonce.
|
||||||
|
* - если подпись корректна, сервер закрывает указанную sessionId (или текущую,
|
||||||
|
* если sessionId не задана) и рвёт соответствующее WebSocket-подключение.
|
||||||
|
*/
|
||||||
|
public class Net_CloseActiveSession_Request extends Net_Request {
|
||||||
|
|
||||||
|
/** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/** Время на стороне клиента (мс с 1970-01-01). Используется при AUTH_IN_PROGRESS. */
|
||||||
|
private long timeMs;
|
||||||
|
|
||||||
|
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
||||||
|
private String signatureB64;
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimeMs() {
|
||||||
|
return timeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeMs(long timeMs) {
|
||||||
|
this.timeMs = timeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSignatureB64() {
|
||||||
|
return signatureB64;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignatureB64(String signatureB64) {
|
||||||
|
this.signatureB64 = signatureB64;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.entyties.Auth;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответ на CloseActiveSession.
|
||||||
|
*
|
||||||
|
* При успехе:
|
||||||
|
* - status = 200;
|
||||||
|
* - payload = {}.
|
||||||
|
*
|
||||||
|
* Закрытие WebSocket-соединения может быть выполнено сразу (для другой сессии)
|
||||||
|
* или чуть позже (для текущей сессии) после отправки ответа.
|
||||||
|
*/
|
||||||
|
public class Net_CloseActiveSession_Response extends Net_Response {
|
||||||
|
// Дополнительных полей пока не требуется.
|
||||||
|
}
|
||||||
@ -0,0 +1,258 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import server.ws.WsConnectionUtils;
|
||||||
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
|
import shine.db.entities.ActiveSession;
|
||||||
|
import shine.db.entities.SolanaUser;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хэндлер CloseActiveSession.
|
||||||
|
*
|
||||||
|
* Назначение:
|
||||||
|
* - закрыть одну из активных сессий пользователя:
|
||||||
|
* * либо явно указанную в sessionId,
|
||||||
|
* * либо текущую (если sessionId не задана).
|
||||||
|
*
|
||||||
|
* Допустимые состояния:
|
||||||
|
* - AUTH_STATUS_USER:
|
||||||
|
* * timeMs / signatureB64 могут быть пустыми.
|
||||||
|
* * Достаточно факта текущей авторизации.
|
||||||
|
*
|
||||||
|
* - AUTH_STATUS_AUTH_IN_PROGRESS:
|
||||||
|
* * требуется проверка подписи Ed25519 над строкой
|
||||||
|
* "AUTHORIFICATED:" + timeMs + authNonce
|
||||||
|
* (authNonce взят на шаге AuthChallenge и хранится в ctx.authNonce).
|
||||||
|
* * Если подпись корректна, можно закрывать сессию даже до полноценной
|
||||||
|
* установки новой сессии.
|
||||||
|
*
|
||||||
|
* Закрытие:
|
||||||
|
* - запись ActiveSession удаляется из БД;
|
||||||
|
* - если по этой sessionId есть активное WebSocket-подключение:
|
||||||
|
* * если это ДРУГОЕ подключение — оно закрывается сразу;
|
||||||
|
* * если это ТЕКУЩЕЕ подключение — сначала отправляется ответ 200,
|
||||||
|
* а закрытие выполняется в отдельном потоке с небольшой задержкой.
|
||||||
|
*/
|
||||||
|
public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
|
||||||
|
|
||||||
|
if (ctx == null || ctx.getSolanaUser() == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"NOT_AUTHENTICATED",
|
||||||
|
"Операция доступна только в состоянии авторизации или авторификации"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SolanaUser user = ctx.getSolanaUser();
|
||||||
|
long currentLoginId = user.getLoginId();
|
||||||
|
|
||||||
|
int authStatus = ctx.getAuthenticationStatus();
|
||||||
|
if (authStatus != ConnectionContext.AUTH_STATUS_USER
|
||||||
|
&& authStatus != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||||
|
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"BAD_AUTH_STATUS",
|
||||||
|
"Операция CloseActiveSession недоступна в текущем статусе аутентификации"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если мы ещё на шаге AUTH_IN_PROGRESS — проверяем подпись
|
||||||
|
if (authStatus == ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||||
|
String authNonce = ctx.getAuthNonce();
|
||||||
|
if (authNonce == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"NO_STEP1_CONTEXT",
|
||||||
|
"Шаг 1 авторизации не был корректно выполнен для данного соединения"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long timeMs = req.getTimeMs();
|
||||||
|
String signatureB64 = req.getSignatureB64();
|
||||||
|
|
||||||
|
if (signatureB64 == null || signatureB64.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"EMPTY_SIGNATURE",
|
||||||
|
"Подпись обязательна при статусе AUTH_IN_PROGRESS"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
long diff = Math.abs(nowMs - timeMs);
|
||||||
|
if (diff > Net_CreateAuthSession__Handler.ALLOWED_SKEW_MS) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"TIME_SKEW",
|
||||||
|
"Время клиента отличается от сервера более чем на 30 секунд"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean sigOk;
|
||||||
|
try {
|
||||||
|
sigOk = Net_CreateAuthSession__Handler.verifyAuthorificatedSignature(
|
||||||
|
user,
|
||||||
|
authNonce,
|
||||||
|
timeMs,
|
||||||
|
signatureB64
|
||||||
|
);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_BASE64",
|
||||||
|
"Некорректный формат Base64 для ключа или подписи"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sigOk) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"BAD_SIGNATURE",
|
||||||
|
"Подпись не прошла проверку"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем, какую sessionId закрывать
|
||||||
|
String targetSessionId = req.getSessionId();
|
||||||
|
if (targetSessionId == null || targetSessionId.isBlank()) {
|
||||||
|
// Если sessionId не передана — берём текущую активную
|
||||||
|
if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
|
||||||
|
targetSessionId = ctx.getActiveSession().getSessionId();
|
||||||
|
} else if (ctx.getSessionId() != null) {
|
||||||
|
targetSessionId = ctx.getSessionId();
|
||||||
|
} else {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"NO_SESSION_TO_CLOSE",
|
||||||
|
"Не удалось определить, какую сессию нужно закрыть"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
||||||
|
ActiveSession targetSession;
|
||||||
|
try {
|
||||||
|
targetSession = sessionsDao.getBySessionId(targetSessionId);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"DB_ERROR",
|
||||||
|
"Ошибка доступа к базе данных при поиске сессии"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetSession == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"SESSION_NOT_FOUND",
|
||||||
|
"Сессия для закрытия не найдена"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetSession.getLoginId() != currentLoginId) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"SESSION_OF_ANOTHER_USER",
|
||||||
|
"Нельзя закрывать сессию другого пользователя"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());
|
||||||
|
|
||||||
|
// Пытаемся удалить сессию из БД и закрыть соответствующее подключение
|
||||||
|
closeActiveSession(targetSessionId, ctx, isCurrentSession);
|
||||||
|
|
||||||
|
// Ответ OK (payload станет {} в JsonInboundProcessor)
|
||||||
|
Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
|
||||||
|
// Для текущей сессии WebSocket будет закрыт чуть позже в отдельном потоке,
|
||||||
|
// чтобы этот ответ успел уйти.
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Закрытие активной сессии:
|
||||||
|
* - удаление записи из БД;
|
||||||
|
* - закрытие WebSocket-подключения, если оно существует.
|
||||||
|
*
|
||||||
|
* @param targetSessionId идентификатор сессии, которую надо закрыть
|
||||||
|
* @param currentCtx контекст текущего подключения (которое вызвало запрос)
|
||||||
|
* @param isCurrentSession true, если закрывается "эта же" сессия
|
||||||
|
*/
|
||||||
|
private void closeActiveSession(String targetSessionId,
|
||||||
|
ConnectionContext currentCtx,
|
||||||
|
boolean isCurrentSession) {
|
||||||
|
|
||||||
|
ActiveSessionsDAO sessionsDao = ActiveSessionsDAO.getInstance();
|
||||||
|
try {
|
||||||
|
sessionsDao.deleteBySessionId(targetSessionId);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
|
||||||
|
// Логируем, но считаем, что для клиента сессия всё равно должна быть недействительна.
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionContext ctxToClose =
|
||||||
|
ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
|
||||||
|
|
||||||
|
if (ctxToClose == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurrentSession && ctxToClose == currentCtx) {
|
||||||
|
// Это текущее подключение: закрываем после отправки ответа.
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(50); // небольшая пауза, чтобы ответ ушёл
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
WsConnectionUtils.closeConnection(
|
||||||
|
ctxToClose,
|
||||||
|
4000,
|
||||||
|
"Session closed by client via CloseActiveSession"
|
||||||
|
);
|
||||||
|
}, "CloseSession-" + targetSessionId).start();
|
||||||
|
} else {
|
||||||
|
// Другая сессия — можно закрыть сразу
|
||||||
|
WsConnectionUtils.closeConnection(
|
||||||
|
ctxToClose,
|
||||||
|
4000,
|
||||||
|
"Session closed by client via CloseActiveSession"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -54,7 +54,39 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
|
private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
|
||||||
|
|
||||||
private static final SecureRandom RANDOM = new SecureRandom();
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
private static final long ALLOWED_SKEW_MS = 30_000L;
|
|
||||||
|
/** Допустимое расхождение времени клиента и сервера (мс). */
|
||||||
|
public static final long ALLOWED_SKEW_MS = 30_000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Общая проверка подписи Ed25519 над строкой:
|
||||||
|
* "AUTHORIFICATED:" + timeMs + authNonce.
|
||||||
|
*
|
||||||
|
* Используется и в CreateAuthSession, и в CloseActiveSession (для статуса AUTH_IN_PROGRESS).
|
||||||
|
*
|
||||||
|
* @param user пользователь (используется deviceKey)
|
||||||
|
* @param authNonce одноразовый nonce из шага 1
|
||||||
|
* @param timeMs время на стороне клиента
|
||||||
|
* @param signatureB64 подпись в base64
|
||||||
|
* @return true — подпись корректна; false — подпись не проходит верификацию
|
||||||
|
* @throws IllegalArgumentException при некорректном base64 ключа/подписи
|
||||||
|
*/
|
||||||
|
public static boolean verifyAuthorificatedSignature(
|
||||||
|
SolanaUser user,
|
||||||
|
String authNonce,
|
||||||
|
long timeMs,
|
||||||
|
String signatureB64
|
||||||
|
) throws IllegalArgumentException {
|
||||||
|
|
||||||
|
String pubKeyB64 = user.getDeviceKey();
|
||||||
|
byte[] publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64);
|
||||||
|
byte[] signature64 = Base64.getDecoder().decode(signatureB64);
|
||||||
|
|
||||||
|
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
||||||
|
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
return Ed25519Util.verify(preimage, signature64, publicKey32);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
@ -147,11 +179,13 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] publicKey32;
|
// --- authNonce (challenge) мы сохранили в ctx.authNonce на шаге 1 ---
|
||||||
byte[] signature64;
|
String authNonce = ctx.getAuthNonce();
|
||||||
|
|
||||||
|
// --- проверяем подпись через общий метод ---
|
||||||
|
boolean sigOk;
|
||||||
try {
|
try {
|
||||||
publicKey32 = Ed25519Util.keyFromBase64(pubKeyB64);
|
sigOk = verifyAuthorificatedSignature(user, authNonce, timeMs, signatureB64);
|
||||||
signature64 = Base64.getDecoder().decode(signatureB64);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
@ -163,14 +197,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- authNonce (challenge) мы сохранили в ctx.authNonce на шаге 1 ---
|
|
||||||
String authNonce = ctx.getAuthNonce();
|
|
||||||
|
|
||||||
// --- собираем строку для подписи: "AUTHORIFICATED:" + timeMs + authNonce ---
|
|
||||||
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
|
||||||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
boolean sigOk = Ed25519Util.verify(preimage, signature64, publicKey32);
|
|
||||||
if (!sigOk) {
|
if (!sigOk) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
|
|||||||
@ -31,12 +31,22 @@ import java.util.concurrent.CountDownLatch;
|
|||||||
* 4) Новое подключение:
|
* 4) Новое подключение:
|
||||||
* - отправляем RefreshSession с тем же sessionId,
|
* - отправляем RefreshSession с тем же sessionId,
|
||||||
* но заведомо неверным sessionPwd
|
* но заведомо неверным sessionPwd
|
||||||
* (в консоль пишем: ожидаем ОТРИЦАТЕЛЬНЫЙ ответ).
|
* (ожидаем ОТРИЦАТЕЛЬНЫЙ ответ: status != 200,
|
||||||
|
* code = SESSION_PWD_MISMATCH).
|
||||||
*
|
*
|
||||||
* 5) Ещё одно новое подключение:
|
* 5) Ещё одно новое подключение:
|
||||||
* - отправляем RefreshSession с sessionId
|
* - отправляем RefreshSession с sessionId
|
||||||
* и корректным sessionPwd
|
* и корректным sessionPwd
|
||||||
* (в консоль пишем: ожидаем УСПЕШНЫЙ ответ).
|
* (ожидаем УСПЕШНЫЙ ответ: status=200,
|
||||||
|
* storagePwd совпадает с тем, что отправляли на шаге 3).
|
||||||
|
*
|
||||||
|
* В ЭТОМ ЖЕ подключении:
|
||||||
|
* - вызываем CloseActiveSession для этой sessionId;
|
||||||
|
* ждём 200 (успешное закрытие сессии).
|
||||||
|
*
|
||||||
|
* 6) Новое подключение:
|
||||||
|
* - снова пытаемся сделать RefreshSession по той же sessionId/sessionPwd;
|
||||||
|
* ожидаем ошибку: status != 200, code = SESSION_NOT_FOUND.
|
||||||
*/
|
*/
|
||||||
public class Test_AddUser_and_Authorification {
|
public class Test_AddUser_and_Authorification {
|
||||||
|
|
||||||
@ -51,7 +61,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
private static final long TEST_BCH_ID = 4222L;
|
private static final long TEST_BCH_ID = 4222L;
|
||||||
private static final int TEST_BCH_LIMIT = 1_000_000;
|
private static final int TEST_BCH_LIMIT = 1_000_000;
|
||||||
|
|
||||||
// Краткая строка clientInfo, которую клиент шлёт на шаге CreateAuthSession
|
// Краткая строка clientInfo, которую клиент шлёт на шаге CreateAuthSession и RefreshSession
|
||||||
private static final String TEST_CLIENT_INFO = "JavaTestClient/1.0";
|
private static final String TEST_CLIENT_INFO = "JavaTestClient/1.0";
|
||||||
|
|
||||||
// --- Тестовые пары ключей ---
|
// --- Тестовые пары ключей ---
|
||||||
@ -87,7 +97,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
/** sessionPwd (секрет сессии), выданный на шаге CreateAuthSession. */
|
/** sessionPwd (секрет сессии), выданный на шаге CreateAuthSession. */
|
||||||
private static String GLOBAL_SESSION_PWD;
|
private static String GLOBAL_SESSION_PWD;
|
||||||
|
|
||||||
/** storagePwd, который мы отправили при CreateAuthSession (для информации). */
|
/** storagePwd, который мы отправили при CreateAuthSession. */
|
||||||
private static String GLOBAL_STORAGE_PWD_SENT;
|
private static String GLOBAL_STORAGE_PWD_SENT;
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
@ -99,8 +109,11 @@ public class Test_AddUser_and_Authorification {
|
|||||||
// Сценарий 2: новое подключение, RefreshSession с неверным sessionPwd
|
// Сценарий 2: новое подключение, RefreshSession с неверным sessionPwd
|
||||||
runScenario_RefreshSession_WrongPwd();
|
runScenario_RefreshSession_WrongPwd();
|
||||||
|
|
||||||
// Сценарий 3: новое подключение, RefreshSession с корректным sessionPwd
|
// Сценарий 3: новое подключение, RefreshSession с корректным sessionPwd + CloseActiveSession
|
||||||
runScenario_RefreshSession_CorrectPwd();
|
runScenario_RefreshSession_CorrectPwd_And_Close();
|
||||||
|
|
||||||
|
// Сценарий 4: новое подключение, RefreshSession после закрытия сессии
|
||||||
|
runScenario_RefreshSession_AfterClose();
|
||||||
|
|
||||||
System.out.println("Все тесты завершены, выходим.");
|
System.out.println("Все тесты завершены, выходим.");
|
||||||
}
|
}
|
||||||
@ -169,19 +182,50 @@ public class Test_AddUser_and_Authorification {
|
|||||||
System.out.println(message);
|
System.out.println(message);
|
||||||
System.out.println("-----------------------------------------------------");
|
System.out.println("-----------------------------------------------------");
|
||||||
|
|
||||||
// Шаг 2: получаем authNonce
|
int status = extractStatus(message);
|
||||||
if (step == 1) {
|
switch (step) {
|
||||||
GLOBAL_AUTH_NONCE = extractAuthNonce(message);
|
case 0 -> {
|
||||||
System.out.println("🔑 [S1] Извлечён authNonce: " + GLOBAL_AUTH_NONCE);
|
// AddUser: ждём status=200
|
||||||
}
|
if (status == 200) {
|
||||||
|
printOk("[S1] AddUser", "Пользователь успешно добавлен (status=200)");
|
||||||
// Шаг 3: получаем sessionId и sessionPwd
|
} else {
|
||||||
if (step == 2) {
|
String code = extractErrorCode(message);
|
||||||
GLOBAL_SESSION_ID = extractSessionId(message);
|
printFail("[S1] AddUser", "Ожидали status=200, получили status=" + status + ", code=" + code);
|
||||||
GLOBAL_SESSION_PWD = extractSessionPwd(message);
|
}
|
||||||
System.out.println("🆔 [S1] Извлечён sessionId: " + GLOBAL_SESSION_ID);
|
}
|
||||||
System.out.println("🔐 [S1] Извлечён sessionPwd: " + GLOBAL_SESSION_PWD);
|
case 1 -> {
|
||||||
System.out.println(" (Эти sessionId и sessionPwd понадобятся в сценариях 2 и 3)");
|
// AuthChallenge: статус 200 + authNonce
|
||||||
|
String nonce = extractAuthNonce(message);
|
||||||
|
GLOBAL_AUTH_NONCE = nonce;
|
||||||
|
if (status == 200 && nonce != null && !nonce.isBlank()) {
|
||||||
|
printOk("[S1] AuthChallenge", "status=200, получен authNonce=" + nonce);
|
||||||
|
} else {
|
||||||
|
String code = extractErrorCode(message);
|
||||||
|
printFail("[S1] AuthChallenge",
|
||||||
|
"Ожидали status=200 + непустой authNonce, получили status="
|
||||||
|
+ status + ", nonce=" + nonce + ", code=" + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2 -> {
|
||||||
|
// CreateAuthSession: статус 200 + sessionId & sessionPwd
|
||||||
|
String sid = extractSessionId(message);
|
||||||
|
String spwd = extractSessionPwd(message);
|
||||||
|
GLOBAL_SESSION_ID = sid;
|
||||||
|
GLOBAL_SESSION_PWD = spwd;
|
||||||
|
if (status == 200 && sid != null && !sid.isBlank()
|
||||||
|
&& spwd != null && !spwd.isBlank()) {
|
||||||
|
printOk("[S1] CreateAuthSession",
|
||||||
|
"status=200, sessionId и sessionPwd получены");
|
||||||
|
} else {
|
||||||
|
String code = extractErrorCode(message);
|
||||||
|
printFail("[S1] CreateAuthSession",
|
||||||
|
"Ожидали status=200 + непустые sessionId/sessionPwd, получили status="
|
||||||
|
+ status + ", sid=" + sid + ", code=" + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
// не должно сюда попадать
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
step++;
|
step++;
|
||||||
@ -219,7 +263,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
private static void runScenario_RefreshSession_WrongPwd() throws Exception {
|
private static void runScenario_RefreshSession_WrongPwd() throws Exception {
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println("=== СЦЕНАРИЙ 2: RefreshSession с НЕВЕРНЫМ sessionPwd ===");
|
System.out.println("=== СЦЕНАРИЙ 2: RefreshSession с НЕВЕРНЫМ sessionPwd ===");
|
||||||
System.out.println("Ожидаем ОТРИЦАТЕЛЬНЫЙ ответ сервера (UNVERIFIED / SESSION_PWD_MISMATCH и т.п.)");
|
System.out.println("Ожидаем ОТРИЦАТЕЛЬНЫЙ ответ сервера: status != 200, code = SESSION_PWD_MISMATCH");
|
||||||
|
|
||||||
if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) {
|
if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) {
|
||||||
System.out.println("⚠️ Нет sessionId или sessionPwd из сценария 1, пропускаем сценарий 2.");
|
System.out.println("⚠️ Нет sessionId или sessionPwd из сценария 1, пропускаем сценарий 2.");
|
||||||
@ -256,7 +300,18 @@ public class Test_AddUser_and_Authorification {
|
|||||||
System.out.println("📥 [S2] Ответ сервера (ожидаем ошибку):");
|
System.out.println("📥 [S2] Ответ сервера (ожидаем ошибку):");
|
||||||
System.out.println(message);
|
System.out.println(message);
|
||||||
System.out.println("-----------------------------------------------------");
|
System.out.println("-----------------------------------------------------");
|
||||||
System.out.println("💬 [S2] Если в ответе status != 200 и/или код ошибки про неверный пароль — это ПРАВИЛЬНОЕ поведение.");
|
|
||||||
|
int status = extractStatus(message);
|
||||||
|
String code = extractErrorCode(message);
|
||||||
|
|
||||||
|
if (status != 200 && "SESSION_PWD_MISMATCH".equals(code)) {
|
||||||
|
printOk("[S2] RefreshSession (wrong pwd)",
|
||||||
|
"Получена ожидаемая ошибка: status=" + status + ", code=" + code);
|
||||||
|
} else {
|
||||||
|
printFail("[S2] RefreshSession (wrong pwd)",
|
||||||
|
"Ожидали status!=200 + code=SESSION_PWD_MISMATCH, получили status="
|
||||||
|
+ status + ", code=" + code);
|
||||||
|
}
|
||||||
|
|
||||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario2 done");
|
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario2 done");
|
||||||
webSocket.request(1);
|
webSocket.request(1);
|
||||||
@ -285,17 +340,17 @@ public class Test_AddUser_and_Authorification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// SCENARIO 3: RefreshSession с правильными данными
|
// SCENARIO 3: RefreshSession OK + CloseActiveSession
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
|
|
||||||
private static void runScenario_RefreshSession_CorrectPwd() throws Exception {
|
private static void runScenario_RefreshSession_CorrectPwd_And_Close() throws Exception {
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println("=== СЦЕНАРИЙ 3: RefreshSession с КОРРЕКТНЫМ sessionPwd ===");
|
System.out.println("=== СЦЕНАРИЙ 3: RefreshSession с КОРРЕКТНЫМ sessionPwd + CloseActiveSession ===");
|
||||||
System.out.println("Ожидаем УСПЕШНЫЙ ответ сервера (status=200),");
|
System.out.println("1) Ожидаем: status=200 и корректный storagePwd");
|
||||||
System.out.println(" а в payload должен вернуться актуальный storagePwd.");
|
System.out.println("2) Затем в этом же подключении вызываем CloseActiveSession для той же sessionId и ждём status=200.");
|
||||||
|
|
||||||
if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) {
|
if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null || GLOBAL_STORAGE_PWD_SENT == null) {
|
||||||
System.out.println("⚠️ Нет sessionId или sessionPwd из сценария 1, пропускаем сценарий 3.");
|
System.out.println("⚠️ Нет необходимых данных из сценария 1, пропускаем сценарий 3.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,6 +360,8 @@ public class Test_AddUser_and_Authorification {
|
|||||||
client.newWebSocketBuilder()
|
client.newWebSocketBuilder()
|
||||||
.buildAsync(URI.create(WS_URI), new Listener() {
|
.buildAsync(URI.create(WS_URI), new Listener() {
|
||||||
|
|
||||||
|
private int step = 0; // 0 - RefreshSession OK, 1 - CloseActiveSession
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOpen(WebSocket webSocket) {
|
public void onOpen(WebSocket webSocket) {
|
||||||
System.out.println("✅ [S3] WebSocket подключен");
|
System.out.println("✅ [S3] WebSocket подключен");
|
||||||
@ -312,7 +369,7 @@ public class Test_AddUser_and_Authorification {
|
|||||||
|
|
||||||
String json = buildRefreshSessionJson(GLOBAL_SESSION_ID, GLOBAL_SESSION_PWD, "test-refresh-ok-1");
|
String json = buildRefreshSessionJson(GLOBAL_SESSION_ID, GLOBAL_SESSION_PWD, "test-refresh-ok-1");
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println("📤 [S3] Отправляем RefreshSession с КОРРЕКТНЫМ sessionPwd:");
|
System.out.println("📤 [S3 / Шаг 1] Отправляем RefreshSession с КОРРЕКТНЫМ sessionPwd:");
|
||||||
System.out.println(json);
|
System.out.println(json);
|
||||||
webSocket.sendText(json, true);
|
webSocket.sendText(json, true);
|
||||||
Listener.super.onOpen(webSocket);
|
Listener.super.onOpen(webSocket);
|
||||||
@ -323,15 +380,52 @@ public class Test_AddUser_and_Authorification {
|
|||||||
CharSequence data,
|
CharSequence data,
|
||||||
boolean last) {
|
boolean last) {
|
||||||
String message = data.toString();
|
String message = data.toString();
|
||||||
System.out.println("📥 [S3] Ответ сервера (ожидаем успех):");
|
System.out.println("📥 [S3] Ответ сервера (step=" + step + "):");
|
||||||
System.out.println(message);
|
System.out.println(message);
|
||||||
System.out.println("-----------------------------------------------------");
|
System.out.println("-----------------------------------------------------");
|
||||||
System.out.println("💬 [S3] Если status=200 — сессия успешно восстановлена.");
|
|
||||||
String storagePwdFromServer = extractStoragePwd(message);
|
|
||||||
System.out.println("🧾 [S3] storagePwd от сервера: " + storagePwdFromServer);
|
|
||||||
System.out.println(" (Должен совпадать с тем, что отправляли в шаге 3 сценария 1)");
|
|
||||||
|
|
||||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario3 done");
|
if (step == 0) {
|
||||||
|
// Ответ на RefreshSession
|
||||||
|
int status = extractStatus(message);
|
||||||
|
String storagePwdFromServer = extractStoragePwd(message);
|
||||||
|
|
||||||
|
if (status == 200 && GLOBAL_STORAGE_PWD_SENT.equals(storagePwdFromServer)) {
|
||||||
|
printOk("[S3] RefreshSession (correct pwd)",
|
||||||
|
"status=200, storagePwd совпадает с отправленным ранее");
|
||||||
|
} else {
|
||||||
|
String code = extractErrorCode(message);
|
||||||
|
printFail("[S3] RefreshSession (correct pwd)",
|
||||||
|
"Ожидали status=200 + storagePwd="
|
||||||
|
+ GLOBAL_STORAGE_PWD_SENT
|
||||||
|
+ ", получили status=" + status
|
||||||
|
+ ", storagePwd=" + storagePwdFromServer
|
||||||
|
+ ", code=" + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Теперь отправляем CloseActiveSession для этой же sessionId
|
||||||
|
String closeJson = buildCloseActiveSessionJson(GLOBAL_SESSION_ID, "test-close-1");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("📤 [S3 / Шаг 2] Отправляем CloseActiveSession для sessionId=" + GLOBAL_SESSION_ID);
|
||||||
|
System.out.println(closeJson);
|
||||||
|
webSocket.sendText(closeJson, true);
|
||||||
|
step = 1;
|
||||||
|
} else if (step == 1) {
|
||||||
|
// Ответ на CloseActiveSession
|
||||||
|
int status = extractStatus(message);
|
||||||
|
String code = extractErrorCode(message);
|
||||||
|
|
||||||
|
if (status == 200) {
|
||||||
|
printOk("[S3] CloseActiveSession",
|
||||||
|
"status=200, сессия закрыта (запись в БД удалена, другие подключения при наличии закрыты)");
|
||||||
|
} else {
|
||||||
|
printFail("[S3] CloseActiveSession",
|
||||||
|
"Ожидали status=200, получили status=" + status + ", code=" + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сервер может сам закрыть WebSocket, но мы тоже корректно закрываем
|
||||||
|
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario3 done");
|
||||||
|
}
|
||||||
|
|
||||||
webSocket.request(1);
|
webSocket.request(1);
|
||||||
return CompletableFuture.completedFuture(null);
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
@ -357,6 +451,86 @@ public class Test_AddUser_and_Authorification {
|
|||||||
System.out.println("=== СЦЕНАРИЙ 3 завершён ===");
|
System.out.println("=== СЦЕНАРИЙ 3 завершён ===");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// SCENARIO 4: RefreshSession после закрытия сессии
|
||||||
|
// ==========================================================
|
||||||
|
|
||||||
|
private static void runScenario_RefreshSession_AfterClose() throws Exception {
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("=== СЦЕНАРИЙ 4: RefreshSession после CloseActiveSession ===");
|
||||||
|
System.out.println("Ожидаем: status != 200, code = SESSION_NOT_FOUND");
|
||||||
|
|
||||||
|
if (GLOBAL_SESSION_ID == null || GLOBAL_SESSION_PWD == null) {
|
||||||
|
System.out.println("⚠️ Нет sessionId или sessionPwd, пропускаем сценарий 4.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
|
client.newWebSocketBuilder()
|
||||||
|
.buildAsync(URI.create(WS_URI), new Listener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket webSocket) {
|
||||||
|
System.out.println("✅ [S4] WebSocket подключен");
|
||||||
|
webSocket.request(1);
|
||||||
|
|
||||||
|
String json = buildRefreshSessionJson(GLOBAL_SESSION_ID, GLOBAL_SESSION_PWD, "test-refresh-after-close-1");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("📤 [S4] Отправляем RefreshSession ПОСЛЕ закрытия сессии:");
|
||||||
|
System.out.println(json);
|
||||||
|
webSocket.sendText(json, true);
|
||||||
|
Listener.super.onOpen(webSocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onText(WebSocket webSocket,
|
||||||
|
CharSequence data,
|
||||||
|
boolean last) {
|
||||||
|
String message = data.toString();
|
||||||
|
System.out.println("📥 [S4] Ответ сервера:");
|
||||||
|
System.out.println(message);
|
||||||
|
System.out.println("-----------------------------------------------------");
|
||||||
|
|
||||||
|
int status = extractStatus(message);
|
||||||
|
String code = extractErrorCode(message);
|
||||||
|
|
||||||
|
if (status != 200 && "SESSION_NOT_FOUND".equals(code)) {
|
||||||
|
printOk("[S4] RefreshSession after Close",
|
||||||
|
"Получена ожидаемая ошибка: status=" + status + ", code=" + code);
|
||||||
|
} else {
|
||||||
|
printFail("[S4] RefreshSession after Close",
|
||||||
|
"Ожидали status!=200 + code=SESSION_NOT_FOUND, получили status="
|
||||||
|
+ status + ", code=" + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario4 done");
|
||||||
|
webSocket.request(1);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(WebSocket webSocket, Throwable error) {
|
||||||
|
System.out.println("❌ [S4] Ошибка WebSocket-клиента: " + error.getMessage());
|
||||||
|
error.printStackTrace(System.out);
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onClose(WebSocket webSocket,
|
||||||
|
int statusCode,
|
||||||
|
String reason) {
|
||||||
|
System.out.println("🔚 [S4] Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
||||||
|
latch.countDown();
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
}).join();
|
||||||
|
|
||||||
|
latch.await();
|
||||||
|
System.out.println("=== СЦЕНАРИЙ 4 завершён ===");
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// JSON BUILDERS
|
// JSON BUILDERS
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
@ -462,6 +636,26 @@ public class Test_AddUser_and_Authorification {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5) CloseActiveSession: можно передать sessionId, timeMs и signatureB64
|
||||||
|
// В нашем случае уже есть авторизованная сессия, поэтому timeMs и signatureB64
|
||||||
|
// можно задать нулями/пустыми — сервер их игнорирует в AUTH_STATUS_USER.
|
||||||
|
private static String buildCloseActiveSessionJson(String sessionId, String requestId) {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"op": "CloseActiveSession",
|
||||||
|
"requestId": "%s",
|
||||||
|
"payload": {
|
||||||
|
"sessionId": "%s",
|
||||||
|
"timeMs": 0,
|
||||||
|
"signatureB64": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
requestId,
|
||||||
|
sessionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// просто для теста: base64 от 32 байт "storage" ключа
|
// просто для теста: base64 от 32 байт "storage" ключа
|
||||||
private static String generateFakeStoragePwd() {
|
private static String generateFakeStoragePwd() {
|
||||||
byte[] data = new byte[32];
|
byte[] data = new byte[32];
|
||||||
@ -526,4 +720,41 @@ public class Test_AddUser_and_Authorification {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int extractStatus(String json) {
|
||||||
|
try {
|
||||||
|
JsonNode root = JSON_MAPPER.readTree(json);
|
||||||
|
if (root.has("status")) {
|
||||||
|
return root.get("status").asInt();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("⚠️ Не удалось распарсить status из ответа: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractErrorCode(String json) {
|
||||||
|
try {
|
||||||
|
JsonNode root = JSON_MAPPER.readTree(json);
|
||||||
|
JsonNode payload = root.get("payload");
|
||||||
|
if (payload != null && payload.has("code") && !payload.get("code").isNull()) {
|
||||||
|
return payload.get("code").asText();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("⚠️ Не удалось распарсить code из ответа: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// PRINT HELPERS
|
||||||
|
// ==========================================================
|
||||||
|
|
||||||
|
private static void printOk(String testName, String details) {
|
||||||
|
System.out.println("✅ " + testName + " — " + details);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printFail(String testName, String details) {
|
||||||
|
System.out.println("❌ " + testName + " — " + details);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user