04 12 25 Версия авторификации где сервер выдовал сессион Ид

This commit is contained in:
AidarKC 2025-12-05 17:35:58 +03:00
parent fc748a744c
commit c9bfa2d01a
23 changed files with 197 additions and 38 deletions

View File

@ -16,9 +16,10 @@ repositories {
} }
dependencies { dependencies {
implementation 'org.eclipse.jetty:jetty-server:11.0.20' implementation 'org.eclipse.jetty:jetty-server:11.0.20' // WS сервер
implementation 'org.eclipse.jetty:jetty-servlet:11.0.20' implementation 'org.eclipse.jetty:jetty-servlet:11.0.20'
implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.20' implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.20'
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' // шифрование implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' // шифрование
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' // json implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' // json

View File

@ -11,7 +11,7 @@ import java.net.http.HttpResponse;
/** /**
* Сервис для геолокации по IP. * Сервис для геолокации по IP.
* *.
* Основной метод: * Основной метод:
* resolveCountryCityOrIp(ip) -> "Country, City" или исходный ip, если не удалось. * resolveCountryCityOrIp(ip) -> "Country, City" или исходный ip, если не удалось.
*/ */

View File

@ -2,7 +2,7 @@ package shine.geo;
/** /**
* Тестовый запуск геолокации. * Тестовый запуск геолокации.
* *.
* Логика: * Логика:
* 1) Если в args[0] передан IP используем его. * 1) Если в args[0] передан IP используем его.
* 2) Иначе пробуем узнать внешний IP текущей машины. * 2) Иначе пробуем узнать внешний IP текущей машины.

View File

@ -16,6 +16,10 @@ repositories {
} }
dependencies { dependencies {
implementation 'org.eclipse.jetty:jetty-server:11.0.20' // WS сервер
implementation 'org.eclipse.jetty:jetty-servlet:11.0.20'
implementation 'org.eclipse.jetty.websocket:websocket-jetty-server:11.0.20'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' // json implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' // json
implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'org.slf4j:slf4j-api:2.0.9'

View File

@ -0,0 +1,123 @@
package server.logic.ws_protocol.JSON;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Реестр активных подключений (только авторизованные).
*.
* Позволяет:
* - получить ConnectionContext по sessionId;
* - получить все активные подключения пользователя по loginId;
* - удалить подключение при закрытии WebSocket.
*.
* найти все подключения пользователя:
* var set = ActiveConnectionsRegistry.getInstance().getByLoginId(loginId);
*.
* найти конкретное подключение по sessionId:
* ConnectionContext ctx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId);
* Session ws = ctx != null ? ctx.getWsSession() : null;
*/
public final class ActiveConnectionsRegistry {
private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry();
public static ActiveConnectionsRegistry getInstance() {
return INSTANCE;
}
private ActiveConnectionsRegistry() {
// singleton
}
// sessionId -> ConnectionContext
private final ConcurrentHashMap<Long, ConnectionContext> bySessionId = new ConcurrentHashMap<>();
// loginId -> множество ConnectionContext для этого пользователя
private final ConcurrentHashMap<Long, Set<ConnectionContext>> byLoginId = new ConcurrentHashMap<>();
/**
* Зарегистрировать авторизованное подключение.
* Ожидается, что в ctx уже выставлены loginId и sessionId.
*/
public void register(ConnectionContext ctx) {
if (ctx == null) return;
Long sessionId = ctx.getSessionId();
Long loginId = ctx.getLoginId();
if (sessionId == null || loginId == null) {
return;
}
bySessionId.put(sessionId, ctx);
byLoginId
.computeIfAbsent(loginId, id -> new CopyOnWriteArraySet<>())
.add(ctx);
}
/**
* Удалить подключение по контексту (например, при onClose).
*/
public void remove(ConnectionContext ctx) {
if (ctx == null) return;
Long sessionId = ctx.getSessionId();
Long loginId = ctx.getLoginId();
if (sessionId != null) {
bySessionId.remove(sessionId);
}
if (loginId != null) {
Set<ConnectionContext> set = byLoginId.get(loginId);
if (set != null) {
set.remove(ctx);
if (set.isEmpty()) {
byLoginId.remove(loginId);
}
}
}
}
/**
* Удалить подключение по sessionId.
*/
public void removeBySessionId(long sessionId) {
ConnectionContext ctx = bySessionId.remove(sessionId);
if (ctx != null) {
Long loginId = ctx.getLoginId();
if (loginId != null) {
Set<ConnectionContext> set = byLoginId.get(loginId);
if (set != null) {
set.remove(ctx);
if (set.isEmpty()) {
byLoginId.remove(loginId);
}
}
}
}
}
/**
* Получить контекст по sessionId.
*/
public ConnectionContext getBySessionId(long sessionId) {
return bySessionId.get(sessionId);
}
/**
* Получить все активные подключения пользователя по loginId.
*/
public Set<ConnectionContext> getByLoginId(long loginId) {
Set<ConnectionContext> set = byLoginId.get(loginId);
if (set == null) {
return Set.of();
}
// CopyOnWriteArraySet безопасно отдавать как есть
return set;
}
}

View File

@ -1,5 +1,6 @@
package server.logic.ws_protocol.JSON; package server.logic.ws_protocol.JSON;
import org.eclipse.jetty.websocket.api.Session;
import shine.db.entities.SolanaUser; import shine.db.entities.SolanaUser;
import shine.db.entities.ActiveSession; import shine.db.entities.ActiveSession;
@ -24,6 +25,22 @@ public class ConnectionContext {
private int authenticationStatus = AUTH_STATUS_NONE; private int authenticationStatus = AUTH_STATUS_NONE;
/**
* WebSocket-сессия Jetty для данного подключения.
* Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту.
*/
private Session wsSession;
// --- WebSocket Session ---
public Session getWsSession() {
return wsSession;
}
public void setWsSession(Session wsSession) {
this.wsSession = wsSession;
}
// --- SolanaUser / ActiveSession --- // --- SolanaUser / ActiveSession ---
public SolanaUser getSolanaUser() { public SolanaUser getSolanaUser() {
@ -96,6 +113,7 @@ public class ConnectionContext {
sessionPwd = null; sessionPwd = null;
authenticationStatus = AUTH_STATUS_NONE; authenticationStatus = AUTH_STATUS_NONE;
wsSession = null;
} }
@Override @Override

View File

@ -16,7 +16,7 @@ import java.util.Map;
/** /**
* JsonHandlerRegistry единое место, где руками регистрируются * JsonHandlerRegistry единое место, где руками регистрируются
* JSON-операции: op handler и op requestClass. * JSON-операции: op handler и op requestClass.
* *.
* Если нужно добавить новый запрос: * Если нужно добавить новый запрос:
* 1) создаёшь класс NetXXXRequest / NetXXXResponse, * 1) создаёшь класс NetXXXRequest / NetXXXResponse,
* 2) создаёшь JsonMessageHandler (NetXXXHandler), * 2) создаёшь JsonMessageHandler (NetXXXHandler),

View File

@ -17,7 +17,7 @@ import java.util.Map;
/** /**
* JsonInboundProcessor обработка JSON-сообщений. * JsonInboundProcessor обработка JSON-сообщений.
* *.
* 1) Парсит общий пакет (op, requestId,...). * 1) Парсит общий пакет (op, requestId,...).
* 2) По op выбирает класс запроса и хэндлер. * 2) По op выбирает класс запроса и хэндлер.
* 3) Маппит JSON NetRequest через ObjectMapper. * 3) Маппит JSON NetRequest через ObjectMapper.

View File

@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
/** /**
* Шаг 2 авторизации: клиент подтверждает владение ключом. * Шаг 2 авторизации: клиент подтверждает владение ключом.
* *.
* JSON: * JSON:
* { * {
* "op": "AuthSessionNewStep2", * "op": "AuthSessionNewStep2",

View File

@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
/** /**
* Ответ на AuthSessionNewStep2. * Ответ на AuthSessionNewStep2.
* *.
* Успешный JSON: * Успешный JSON:
* { * {
* "op": "AuthSessionNewStep2", * "op": "AuthSessionNewStep2",

View File

@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
/** /**
* Запрос SessionRefresh. * Запрос SessionRefresh.
* *.
* JSON (payload): * JSON (payload):
* { * {
* "sessionId": 123, * "sessionId": 123,

View File

@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetResponse;
/** /**
* Успешный ответ на SessionRefresh. * Успешный ответ на SessionRefresh.
* *.
* Дополнительных полей нет, достаточно status=200 и (опционально) пустого payload. * Дополнительных полей нет, достаточно status=200 и (опционально) пустого payload.
*/ */
public class NetSessionRefreshResponse extends NetResponse { public class NetSessionRefreshResponse extends NetResponse {

View File

@ -3,7 +3,7 @@ package server.logic.ws_protocol.JSON.entyties;
/** /**
* Базовый класс для всех событий (event). * Базовый класс для всех событий (event).
* Общие поля: op и payload. * Общие поля: op и payload.
* *.
* Формат JSON (event): * Формат JSON (event):
* { * {
* "op": "...", * "op": "...",

View File

@ -2,7 +2,7 @@ package server.logic.ws_protocol.JSON.entyties;
/** /**
* Ответ с ошибкой (любой отказ). * Ответ с ошибкой (любой отказ).
* *.
* В payload будет: * В payload будет:
* { * {
* "code": "...", * "code": "...",

View File

@ -2,9 +2,9 @@ package server.logic.ws_protocol.JSON.entyties;
/** /**
* Базовый класс для всех запросов (client server). * Базовый класс для всех запросов (client server).
* *.
* Наследуется от NetEvent и добавляет requestId. * Наследуется от NetEvent и добавляет requestId.
* *.
* Формат JSON (request): * Формат JSON (request):
* { * {
* "op": "...", * "op": "...",

View File

@ -2,9 +2,9 @@ package server.logic.ws_protocol.JSON.entyties;
/** /**
* Базовый класс для всех ответов (server client). * Базовый класс для всех ответов (server client).
* *.
* Наследуется от NetRequest и добавляет status. * Наследуется от NetRequest и добавляет status.
* *.
* Формат JSON (response): * Формат JSON (response):
* { * {
* "op": "...", * "op": "...",

View File

@ -4,7 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.NetRequest;
/** /**
* Запрос AddUser. * Запрос AddUser.
* *.
* Ожидаемый JSON: * Ожидаемый JSON:
* { * {
* "op": "AddUser", * "op": "AddUser",

View File

@ -2,6 +2,7 @@ package server.logic.ws_protocol.JSON.handlers.auth;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.NetRequest; import server.logic.ws_protocol.JSON.entyties.NetRequest;
import server.logic.ws_protocol.JSON.entyties.NetResponse; import server.logic.ws_protocol.JSON.entyties.NetResponse;
@ -22,7 +23,7 @@ import java.util.concurrent.ThreadLocalRandom;
/** /**
* Шаг 2 авторизации: проверка подписи и создание сессии. * Шаг 2 авторизации: проверка подписи и создание сессии.
* *.
* Клиент присылает: * Клиент присылает:
* - loginId * - loginId
* - sigNum (0 или 1) * - sigNum (0 или 1)
@ -165,6 +166,9 @@ public class NetAuthSessionNewStep2Handler implements JsonMessageHandler {
ctx.setSessionId(sessionId); ctx.setSessionId(sessionId);
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
// Регистрируем это подключение в глобальном реестре активных соединений
ActiveConnectionsRegistry.getInstance().register(ctx);
// --- формируем ответ --- // --- формируем ответ ---
NetAuthSessionNewStep2Response resp = new NetAuthSessionNewStep2Response(); NetAuthSessionNewStep2Response resp = new NetAuthSessionNewStep2Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());

View File

@ -2,6 +2,7 @@ package server.logic.ws_protocol.JSON.handlers.auth;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.NetRequest; import server.logic.ws_protocol.JSON.entyties.NetRequest;
import server.logic.ws_protocol.JSON.entyties.NetResponse; import server.logic.ws_protocol.JSON.entyties.NetResponse;
@ -108,6 +109,9 @@ public class NetSessionRefreshHandler implements JsonMessageHandler {
ctx.setSessionId(sessionId); ctx.setSessionId(sessionId);
ctx.setSessionPwd(sessionPwd); ctx.setSessionPwd(sessionPwd);
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
// Регистрируем это подключение в глобальном реестре активных соединений
ActiveConnectionsRegistry.getInstance().register(ctx);
} }
// И возвращаем OK без доп. полей (payload будет {}). // И возвращаем OK без доп. полей (payload будет {}).

View File

@ -2,11 +2,11 @@ package server.logic.ws_protocol;
/** /**
* WireCodes константы бинарного протокола поверх WebSocket. * WireCodes константы бинарного протокола поверх WebSocket.
* *.
* Формат входящего сообщения: * Формат входящего сообщения:
* [4] int opCode (big-endian) * [4] int opCode (big-endian)
* [*] payload * [*] payload
* *.
* Ответ сервера: * Ответ сервера:
* ровно [4] int statusCode (big-endian) * ровно [4] int statusCode (big-endian)
*/ */

View File

@ -1,20 +1,20 @@
* ============================================================================ * ============================================================================
* BchBlockEntry — универсальная запись блокчейна SHiNE (.bch) * BchBlockEntry — универсальная запись блокчейна SHiNE (.bch)
* ============================================================================ * ============================================================================
* *.
* 🧩 Формат файла .bch: * 🧩 Формат файла .bch:
* Каждый блок хранится последовательно, без промежутков. * Каждый блок хранится последовательно, без промежутков.
* Один блок = «заголовок» (RAW) + подпись (64) + хэш (32). * Один блок = «заголовок» (RAW) + подпись (64) + хэш (32).
* *.
* FULL = RAW + signature(64) + hash(32) * FULL = RAW + signature(64) + hash(32)
* *.
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* 🔹 Структура RAW-части блока (без подписи и хэша) * 🔹 Структура RAW-части блока (без подписи и хэша)
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* Размеры и порядок строго фиксированы (BigEndian). * Размеры и порядок строго фиксированы (BigEndian).
* *.
* Порядок байтов (сверху вниз, смещения от начала RAW): * Порядок байтов (сверху вниз, смещения от начала RAW):
* *.
* ┌────────────────────────────┬────────┬───────────────────────────────┐ * ┌────────────────────────────┬────────┬───────────────────────────────┐
* │ Поле │ Размер │ Описание │ * │ Поле │ Размер │ Описание │
* ├────────────────────────────┼────────┼───────────────────────────────┤ * ├────────────────────────────┼────────┼───────────────────────────────┤
@ -30,22 +30,22 @@
* │ recordTypeVersion │ 2 байта│ версия структуры данного типа │ * │ recordTypeVersion │ 2 байта│ версия структуры данного типа │
* │ body │ M байт │ бинарное тело записи │ * │ body │ M байт │ бинарное тело записи │
* └────────────────────────────┴────────┴───────────────────────────────┘ * └────────────────────────────┴────────┴───────────────────────────────┘
* *.
* ⇒ RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 2 = 20 байт. * ⇒ RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 2 = 20 байт.
* ⇒ recordSize = RAW_HEADER_SIZE + body.length * ⇒ recordSize = RAW_HEADER_SIZE + body.length
* *.
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* 🔹 Структура FULL-блока * 🔹 Структура FULL-блока
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* *.
* ┌────────────────────────────┬─────────┬──────────────────────────────┐ * ┌────────────────────────────┬─────────┬──────────────────────────────┐
* │ RAW │ M+20 │ тело блока без подписи │ * │ RAW │ M+20 │ тело блока без подписи │
* │ signature64 │ 64 │ подпись Ed25519(preimage) │ * │ signature64 │ 64 │ подпись Ed25519(preimage) │
* │ hash32 │ 32 │ SHA-256(preimage) │ * │ hash32 │ 32 │ SHA-256(preimage) │
* └────────────────────────────┴─────────┴──────────────────────────────┘ * └────────────────────────────┴─────────┴──────────────────────────────┘
* *.
* ⇒ Общая длина FULL = recordSize + 96 байт. * ⇒ Общая длина FULL = recordSize + 96 байт.
* *.
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* 🔹 Канонический preimage для подписи/хэша * 🔹 Канонический preimage для подписи/хэша
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
@ -58,9 +58,9 @@
* можно номер блока? * можно номер блока?
* prevHash32(32B) + * prevHash32(32B) +
* rawBytes (M+20B) * rawBytes (M+20B)
* *.
* hash32 = SHA-256(preimage) * hash32 = SHA-256(preimage)
* signature64= Ed25519.sign(preimage, privateKey) * signature64= Ed25519.sign(preimage, privateKey)
* *.
* Проверка осуществляется через {@link utils.crypto.BchCryptoVerifier}. * Проверка осуществляется через {@link utils.crypto.BchCryptoVerifier}.

View File

@ -20,7 +20,7 @@ import java.util.Arrays;
* AddBlockHandler обработчик команды "добавить блок" (ADD_BLOCK) * AddBlockHandler обработчик команды "добавить блок" (ADD_BLOCK)
* --------------------------------------------------------------- * ---------------------------------------------------------------
* Принимает бинарное сообщение от клиента и добавляет новый блок в цепочку. * Принимает бинарное сообщение от клиента и добавляет новый блок в цепочку.
* *.
* Формат входного сообщения (msg): * Формат входного сообщения (msg):
* [0..3] 4 байта: код операции (WireCodes.ADD_BLOCK) * [0..3] 4 байта: код операции (WireCodes.ADD_BLOCK)
* [4..11] 8 байт: blockchainId (уникальный идентификатор цепочки) * [4..11] 8 байт: blockchainId (уникальный идентификатор цепочки)
@ -33,13 +33,13 @@ import java.util.Arrays;
* M байт body (содержимое блока) * M байт body (содержимое блока)
* 64 байта signature (Ed25519) * 64 байта signature (Ed25519)
* 32 байта hash (SHA-256) * 32 байта hash (SHA-256)
* *.
* --------------------------------------------------------------- * ---------------------------------------------------------------
* Алгоритм работы: * Алгоритм работы:
* *.
* 1 Распаковать BchBlockEntry из msg (т.е. выделить тело блока и подписи). * 1 Распаковать BchBlockEntry из msg (т.е. выделить тело блока и подписи).
* 2 Найти описание цепочки (BchInfoEntry) по blockchainId. * 2 Найти описание цепочки (BchInfoEntry) по blockchainId.
* *.
* Если описания нет (цепочка ещё не существует): * Если описания нет (цепочка ещё не существует):
* принимаем только блок типа 0 (HeaderBody) и номера 0; * принимаем только блок типа 0 (HeaderBody) и номера 0;
* парсим его, создаём новый BchInfoEntry на основе данных заголовка; * парсим его, создаём новый BchInfoEntry на основе данных заголовка;
@ -48,16 +48,16 @@ import java.util.Arrays;
* сохраняем блок и создаём новый blockchain-файл; * сохраняем блок и создаём новый blockchain-файл;
* добавляем цепочку в менеджер BchInfoManager. * добавляем цепочку в менеджер BchInfoManager.
* (💡 временное решение: создание цепочки допустимо только через HeaderBody) * (💡 временное решение: создание цепочки допустимо только через HeaderBody)
* *.
* Если цепочка уже существует: * Если цепочка уже существует:
* проверяем, что номер блока равен (lastBlockNumber + 1); * проверяем, что номер блока равен (lastBlockNumber + 1);
* проверяем подпись и хэш; * проверяем подпись и хэш;
* проверяем тело блока (check); * проверяем тело блока (check);
* добавляем блок в файл цепочки; * добавляем блок в файл цепочки;
* обновляем состояние BchInfoEntry (номер, хэш, размер). * обновляем состояние BchInfoEntry (номер, хэш, размер).
* *.
* 3 Если все проверки пройдены возвращаем статус OK. * 3 Если все проверки пройдены возвращаем статус OK.
* *.
* Таким образом, единственное различие между первым блоком и последующими * Таким образом, единственное различие между первым блоком и последующими
* момент инициализации описания цепочки (BchInfoEntry). * момент инициализации описания цепочки (BchInfoEntry).
* Всё остальное (валидация, подпись, добавление, обновление) выполняется одинаково. * Всё остальное (валидация, подпись, добавление, обновление) выполняется одинаково.

View File

@ -6,6 +6,7 @@ import org.eclipse.jetty.websocket.api.annotations.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import server.logic.InboundMessageProcessor; import server.logic.InboundMessageProcessor;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.JsonInboundProcessor; import server.logic.ws_protocol.JSON.JsonInboundProcessor;
@ -24,6 +25,8 @@ public class BlockchainWsEndpoint {
@OnWebSocketConnect @OnWebSocketConnect
public void onConnect(Session session) { public void onConnect(Session session) {
this.session = session; this.session = session;
// Привязываем WebSocket-сессию к ConnectionContext
connectionContext.setWsSession(session);
log.info("WS connected: {}", session.getRemoteAddress()); log.info("WS connected: {}", session.getRemoteAddress());
} }
@ -77,6 +80,8 @@ public class BlockchainWsEndpoint {
@OnWebSocketClose @OnWebSocketClose
public void onClose(int statusCode, String reason) { public void onClose(int statusCode, String reason) {
log.info("WS closed: {} {}", statusCode, reason); log.info("WS closed: {} {}", statusCode, reason);
// Удаляем это подключение из реестра активных соединений
ActiveConnectionsRegistry.getInstance().remove(connectionContext);
// На всякий случай очищаем контекст // На всякий случай очищаем контекст
connectionContext.reset(); connectionContext.reset();
} }