Merge branch 'main' of https://github.com/ai5590/SHiNE-server
# Conflicts: # shine-UI/js/app.js
This commit is contained in:
commit
6a9746c17a
3
.gitignore
vendored
3
.gitignore
vendored
@ -47,3 +47,6 @@ bin/
|
|||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# временный debug token
|
||||||
|
.debug-token
|
||||||
|
|||||||
18
AGENT_DEBUG_RUNBOOK.md
Normal file
18
AGENT_DEBUG_RUNBOOK.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Runbook для агента: тест сетевого соединения
|
||||||
|
|
||||||
|
## Быстрый цикл
|
||||||
|
1. Убедись, что сервер запущен.
|
||||||
|
2. Скажи пользователю: «Запусти двух клиентов и напиши “продолжай”».
|
||||||
|
3. По команде «продолжай»:
|
||||||
|
- вызови `GET /debug/ws/clients`,
|
||||||
|
- выбери 2 активные сессии (предпочтительно разных логинов),
|
||||||
|
- вызови `POST /debug/ws/connect`,
|
||||||
|
- получи `runId`.
|
||||||
|
4. Читай `GET /debug/ws/logs?limit=200&runId=<runId>` и сообщай прогресс.
|
||||||
|
5. Если неуспех — покажи ошибки, предложи перезапуск 2 клиентов и повтори.
|
||||||
|
|
||||||
|
## Формат взаимодействия с пользователем
|
||||||
|
- Старт: «Сервер готов. Запусти двух клиентов и скажи “продолжай”.»
|
||||||
|
- После старта run: «Тест запущен, runId=..., проверяю логи.»
|
||||||
|
- Успех: «Соединение установлено, вижу connected.»
|
||||||
|
- Неуспех: «Соединение не поднялось, причины: ... Предлагаю перезапустить клиентов и повторить.»
|
||||||
92
DEBUG_CONNECTION_TESTING.md
Normal file
92
DEBUG_CONNECTION_TESTING.md
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# DEBUG: тестирование сетевого соединения между двумя клиентами
|
||||||
|
|
||||||
|
Документ описывает временный debug-контур для проверки WebRTC соединения между двумя активными WS-сессиями.
|
||||||
|
|
||||||
|
## 1) Подготовка
|
||||||
|
|
||||||
|
|
||||||
|
0. Убедись, что в `application.properties` включен параметр:
|
||||||
|
`debug.tempApi.enabled=true`
|
||||||
|
1. Создай файл `.debug-token` в корне проекта на основе `debug-token.example`.
|
||||||
|
2. В `.debug-token` должна быть одна строка: секретный токен.
|
||||||
|
3. Перезапусти сервер.
|
||||||
|
|
||||||
|
## 2) API debug
|
||||||
|
|
||||||
|
Базовый заголовок для всех запросов:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 Получить список живых клиентов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s \
|
||||||
|
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
|
||||||
|
http://localhost:7070/debug/ws/clients | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Ответ содержит `sessionId`, `login`, `ip`, `userAgent`, и клиентскую информацию.
|
||||||
|
|
||||||
|
### 2.2 Запустить debug-соединение между двумя сессиями
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"initiatorSessionId": "SESSION_ID_A",
|
||||||
|
"responderSessionId": "SESSION_ID_B",
|
||||||
|
"clearDebugLog": false
|
||||||
|
}' \
|
||||||
|
http://localhost:7070/debug/ws/connect | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
В ответе придёт `runId`. Его используй для фильтра логов.
|
||||||
|
|
||||||
|
### 2.3 Читать последние N debug-логов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s \
|
||||||
|
-H "Authorization: Bearer <YOUR_DEBUG_TOKEN>" \
|
||||||
|
"http://localhost:7070/debug/ws/logs?limit=200&runId=<RUN_ID>" | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Операционный сценарий “Codex + пользователь”
|
||||||
|
|
||||||
|
1. Codex поднимает сервер и сообщает пользователю ссылку на UI.
|
||||||
|
2. Codex пишет пользователю: **«Запусти двух клиентов и скажи “продолжай”»**.
|
||||||
|
3. Пользователь запускает два клиента (лучше под разными логинами).
|
||||||
|
4. Пользователь пишет: **«продолжай»**.
|
||||||
|
5. Codex:
|
||||||
|
- вызывает `/debug/ws/clients`,
|
||||||
|
- выбирает 2 сессии,
|
||||||
|
- вызывает `/debug/ws/connect`,
|
||||||
|
- получает `runId`,
|
||||||
|
- читает `/debug/ws/logs?runId=...` и сообщает прогресс.
|
||||||
|
6. Если соединение не удалось:
|
||||||
|
- Codex сообщает ошибки по логам,
|
||||||
|
- при необходимости просит перезапустить 2 клиента,
|
||||||
|
- повторяет запуск debug-run.
|
||||||
|
|
||||||
|
## 4) Какие сообщения считать успехом
|
||||||
|
|
||||||
|
- `peer_connection_connected`
|
||||||
|
- `debug_connection_success`
|
||||||
|
- `signal_sent_200/210/220` без ошибок
|
||||||
|
|
||||||
|
## 5) Что говорить пользователю в ходе прогона (через «колонку»/чат)
|
||||||
|
|
||||||
|
Рекомендуемые фразы:
|
||||||
|
|
||||||
|
- «Сервер запущен. Запусти двух клиентов и напиши “продолжай”.»
|
||||||
|
- «Вижу 2 активные сессии, запускаю тест соединения.»
|
||||||
|
- «Тест запущен, runId=... Сейчас проверяю логи.»
|
||||||
|
- «Соединение установлено / не установлено. Ниже причины и следующий шаг.»
|
||||||
|
|
||||||
|
## 6) Ограничения
|
||||||
|
|
||||||
|
- Механизм временный, не для production-эксплуатации.
|
||||||
|
- Доступ к debug API имеет любой, кто знает токен.
|
||||||
|
- Рекомендуется тестить между разными логинами.
|
||||||
14
debug-token.example
Normal file
14
debug-token.example
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Временный debug token для /debug/ws/* API.
|
||||||
|
#
|
||||||
|
# Как использовать:
|
||||||
|
# 1) Скопируй этот файл в .debug-token
|
||||||
|
# 2) Замени значение ниже на секретную строку
|
||||||
|
# 3) Перезапусти сервер
|
||||||
|
# 4) Передавай токен в заголовке:
|
||||||
|
# Authorization: Bearer <token>
|
||||||
|
#
|
||||||
|
# ВНИМАНИЕ:
|
||||||
|
# - файл .debug-token не должен попадать в git
|
||||||
|
# - механизм временный, для ручных/автотестов соединения
|
||||||
|
|
||||||
|
CHANGE_ME_DEBUG_TOKEN
|
||||||
@ -3,7 +3,13 @@ import { renderToolbar } from './components/toolbar.js';
|
|||||||
import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js';
|
import { captureClientError, setClientErrorTransport } from './services/client-error-reporter.js';
|
||||||
import { initPwaInstallPromptHandling } from './services/pwa-install-service.js';
|
import { initPwaInstallPromptHandling } from './services/pwa-install-service.js';
|
||||||
import { initPwaPush } from './services/pwa-push-service.js';
|
import { initPwaPush } from './services/pwa-push-service.js';
|
||||||
import { handleIncomingCallInvite, handleIncomingCallSignal } from './services/call-service.js';
|
import {
|
||||||
|
handleIncomingCallInvite,
|
||||||
|
handleIncomingCallSignal,
|
||||||
|
setCallDebugReporter,
|
||||||
|
startDebugConnectionAsInitiator,
|
||||||
|
startDebugConnectionAsResponder,
|
||||||
|
} from './services/call-service.js';
|
||||||
import {
|
import {
|
||||||
authService,
|
authService,
|
||||||
addAppLogEntry,
|
addAppLogEntry,
|
||||||
@ -104,12 +110,14 @@ let currentCleanup = null;
|
|||||||
let pingIntervalId = null;
|
let pingIntervalId = null;
|
||||||
let versionCheckIntervalId = null;
|
let versionCheckIntervalId = null;
|
||||||
let versionCheckInFlight = false;
|
let versionCheckInFlight = false;
|
||||||
|
let reconnectIntervalId = null;
|
||||||
let sessionRuntimeStarted = false;
|
let sessionRuntimeStarted = false;
|
||||||
let connectionStatusEl = null;
|
let connectionStatusEl = null;
|
||||||
let connectionState = '';
|
let connectionState = '';
|
||||||
|
|
||||||
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
||||||
initPwaInstallPromptHandling();
|
initPwaInstallPromptHandling();
|
||||||
|
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
||||||
|
|
||||||
function ensureConnectionStatusEl() {
|
function ensureConnectionStatusEl() {
|
||||||
if (connectionStatusEl) return connectionStatusEl;
|
if (connectionStatusEl) return connectionStatusEl;
|
||||||
@ -219,6 +227,16 @@ function startConnectionMonitor() {
|
|||||||
}, CONNECTION_CHECK_INTERVAL_MS);
|
}, CONNECTION_CHECK_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isReconnectAllowedNow() {
|
||||||
|
const pageId = getRoute().pageId || '';
|
||||||
|
return !PRE_AUTH_PAGES.includes(pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsIsOpen() {
|
||||||
|
const ws = authService?.ws?.ws;
|
||||||
|
return !!(ws && ws.readyState === WebSocket.OPEN);
|
||||||
|
}
|
||||||
|
|
||||||
function showGlobalErrorAlert(title, details = {}) {
|
function showGlobalErrorAlert(title, details = {}) {
|
||||||
const lines = [title];
|
const lines = [title];
|
||||||
if (details.message) lines.push(`Сообщение: ${details.message}`);
|
if (details.message) lines.push(`Сообщение: ${details.message}`);
|
||||||
@ -411,6 +429,32 @@ async function ensureSessionRuntimeStarted() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
startConnectionMonitor();
|
startConnectionMonitor();
|
||||||
|
|
||||||
|
if (reconnectIntervalId) {
|
||||||
|
window.clearInterval(reconnectIntervalId);
|
||||||
|
reconnectIntervalId = null;
|
||||||
|
}
|
||||||
|
reconnectIntervalId = window.setInterval(async () => {
|
||||||
|
if (!state.session.isAuthorized) return;
|
||||||
|
if (!isReconnectAllowedNow()) return;
|
||||||
|
if (wsIsOpen()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authService.ws.open();
|
||||||
|
addAppLogEntry({
|
||||||
|
level: 'info',
|
||||||
|
source: 'ws-reconnect',
|
||||||
|
message: 'WS переподключен автоматически',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
addAppLogEntry({
|
||||||
|
level: 'warn',
|
||||||
|
source: 'ws-reconnect',
|
||||||
|
message: 'Попытка автопереподключения не удалась',
|
||||||
|
details: { error: error?.message || 'unknown' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 15_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@ -423,6 +467,10 @@ async function init() {
|
|||||||
setSessionResetHandler(() => {
|
setSessionResetHandler(() => {
|
||||||
sessionRuntimeStarted = false;
|
sessionRuntimeStarted = false;
|
||||||
startConnectionMonitor();
|
startConnectionMonitor();
|
||||||
|
if (reconnectIntervalId) {
|
||||||
|
window.clearInterval(reconnectIntervalId);
|
||||||
|
reconnectIntervalId = null;
|
||||||
|
}
|
||||||
navigate('start-view');
|
navigate('start-view');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -564,6 +612,68 @@ async function init() {
|
|||||||
try { await handleIncomingCallSignal(evt); } catch {}
|
try { await handleIncomingCallSignal(evt); } catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
authService.onEvent('DebugConnectPrepareResponder', async (evt) => {
|
||||||
|
try {
|
||||||
|
const p = evt?.payload || {};
|
||||||
|
await startDebugConnectionAsResponder({
|
||||||
|
runId: p.runId,
|
||||||
|
callId: p.callId,
|
||||||
|
peerLogin: p.peerLogin,
|
||||||
|
peerSessionId: p.peerSessionId,
|
||||||
|
});
|
||||||
|
addAppLogEntry({
|
||||||
|
level: 'info',
|
||||||
|
source: 'debug-connect',
|
||||||
|
message: 'Получена команда debug responder',
|
||||||
|
details: p,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
addAppLogEntry({
|
||||||
|
level: 'error',
|
||||||
|
source: 'debug-connect',
|
||||||
|
message: 'Ошибка запуска debug responder',
|
||||||
|
details: { error: error?.message || 'unknown' },
|
||||||
|
});
|
||||||
|
await authService.reportClientDebug({
|
||||||
|
runId: evt?.payload?.runId || '',
|
||||||
|
level: 'error',
|
||||||
|
message: 'debug_responder_failed',
|
||||||
|
details: error?.message || 'unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
authService.onEvent('DebugConnectStartInitiator', async (evt) => {
|
||||||
|
try {
|
||||||
|
const p = evt?.payload || {};
|
||||||
|
await startDebugConnectionAsInitiator({
|
||||||
|
runId: p.runId,
|
||||||
|
callId: p.callId,
|
||||||
|
peerLogin: p.peerLogin,
|
||||||
|
peerSessionId: p.peerSessionId,
|
||||||
|
});
|
||||||
|
addAppLogEntry({
|
||||||
|
level: 'info',
|
||||||
|
source: 'debug-connect',
|
||||||
|
message: 'Получена команда debug initiator',
|
||||||
|
details: p,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
addAppLogEntry({
|
||||||
|
level: 'error',
|
||||||
|
source: 'debug-connect',
|
||||||
|
message: 'Ошибка запуска debug initiator',
|
||||||
|
details: { error: error?.message || 'unknown' },
|
||||||
|
});
|
||||||
|
await authService.reportClientDebug({
|
||||||
|
runId: evt?.payload?.runId || '',
|
||||||
|
level: 'error',
|
||||||
|
message: 'debug_initiator_failed',
|
||||||
|
details: error?.message || 'unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await tryAutoLogin();
|
await tryAutoLogin();
|
||||||
await hydrateMessagesFromStore();
|
await hydrateMessagesFromStore();
|
||||||
startVersionMonitor();
|
startVersionMonitor();
|
||||||
|
|||||||
@ -1639,6 +1639,16 @@ export class AuthService {
|
|||||||
return response.payload || {};
|
return response.payload || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async reportClientDebug({ runId = '', level = 'info', message = '', details = '' } = {}) {
|
||||||
|
try {
|
||||||
|
const response = await this.ws.request('ClientDebugLog', { runId, level, message, details }, 3000);
|
||||||
|
return response?.status === 200;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async reportClientError(details) {
|
async reportClientError(details) {
|
||||||
try {
|
try {
|
||||||
const response = await this.ws.request('ClientErrorLog', details || {}, 3000);
|
const response = await this.ws.request('ClientErrorLog', details || {}, 3000);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { addChatMessage, state, authService } from '../state.js';
|
import { addChatMessage, authService } from '../state.js';
|
||||||
|
|
||||||
const TYPES = {
|
const TYPES = {
|
||||||
INVITE: 100,
|
INVITE: 100,
|
||||||
@ -14,6 +14,7 @@ const TYPES = {
|
|||||||
|
|
||||||
const calls = new Map();
|
const calls = new Map();
|
||||||
let activeCallId = '';
|
let activeCallId = '';
|
||||||
|
let debugReporter = null;
|
||||||
|
|
||||||
function nowMs() {
|
function nowMs() {
|
||||||
return Date.now();
|
return Date.now();
|
||||||
@ -27,9 +28,26 @@ function getCall(callId) {
|
|||||||
return calls.get(callId) || null;
|
return calls.get(callId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toErrorText(error) {
|
||||||
|
return error?.message || String(error || 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function emitDebug(call, level, message, details = '') {
|
||||||
|
if (!call?.debugRunId || typeof debugReporter !== 'function') return;
|
||||||
|
try {
|
||||||
|
await debugReporter({
|
||||||
|
runId: call.debugRunId,
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
details,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
function setStatus(call, text) {
|
function setStatus(call, text) {
|
||||||
call.status = text;
|
call.status = text;
|
||||||
addChatMessage(call.peerLogin, `[call] ${text}`);
|
addChatMessage(call.peerLogin || 'debug-peer', `[call] ${text}`);
|
||||||
|
void emitDebug(call, 'info', `call_status: ${text}`, `callId=${call.callId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupTimers(call) {
|
function cleanupTimers(call) {
|
||||||
@ -61,10 +79,30 @@ async function finishCall(call, reason, notifyRemote = false) {
|
|||||||
}
|
}
|
||||||
await closeMedia(call);
|
await closeMedia(call);
|
||||||
setStatus(call, `завершен: ${reason}`);
|
setStatus(call, `завершен: ${reason}`);
|
||||||
|
if (String(reason || '').toLowerCase().includes('connected')) {
|
||||||
|
await emitDebug(call, 'info', 'debug_connection_success', reason);
|
||||||
|
}
|
||||||
calls.delete(call.callId);
|
calls.delete(call.callId);
|
||||||
if (activeCallId === call.callId) activeCallId = '';
|
if (activeCallId === call.callId) activeCallId = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendSignal(call, type, data = '') {
|
||||||
|
if (!call.remoteSessionId) return;
|
||||||
|
try {
|
||||||
|
await authService.callSignalToSession({
|
||||||
|
toLogin: call.peerLogin,
|
||||||
|
targetSessionId: call.remoteSessionId,
|
||||||
|
callId: call.callId,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
await emitDebug(call, 'info', `signal_sent_${type}`, `len=${String(data || '').length}`);
|
||||||
|
} catch (error) {
|
||||||
|
await emitDebug(call, 'error', `signal_send_failed_${type}`, toErrorText(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensurePeerConnection(call) {
|
async function ensurePeerConnection(call) {
|
||||||
if (call.pc) return call.pc;
|
if (call.pc) return call.pc;
|
||||||
|
|
||||||
@ -72,25 +110,45 @@ async function ensurePeerConnection(call) {
|
|||||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (call.debugMode && call.debugRole === 'initiator') {
|
||||||
|
const dc = pc.createDataChannel('debug-ping');
|
||||||
|
dc.onopen = () => {
|
||||||
|
try { dc.send('ping'); } catch {}
|
||||||
|
void emitDebug(call, 'info', 'debug_datachannel_open', 'sent ping');
|
||||||
|
};
|
||||||
|
dc.onmessage = (evt) => {
|
||||||
|
void emitDebug(call, 'info', 'debug_datachannel_message', String(evt?.data || ''));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.ondatachannel = (evt) => {
|
||||||
|
const ch = evt?.channel;
|
||||||
|
if (!ch) return;
|
||||||
|
ch.onmessage = (msg) => {
|
||||||
|
const incoming = String(msg?.data || '');
|
||||||
|
void emitDebug(call, 'info', 'debug_datachannel_message_in', incoming);
|
||||||
|
if (incoming === 'ping') {
|
||||||
|
try { ch.send('pong'); } catch {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
pc.onicecandidate = async (event) => {
|
pc.onicecandidate = async (event) => {
|
||||||
if (!event.candidate || !call.remoteSessionId) return;
|
if (!event.candidate || !call.remoteSessionId) return;
|
||||||
try {
|
try {
|
||||||
await authService.callSignalToSession({
|
await sendSignal(call, TYPES.ICE, JSON.stringify(event.candidate));
|
||||||
toLogin: call.peerLogin,
|
|
||||||
targetSessionId: call.remoteSessionId,
|
|
||||||
callId: call.callId,
|
|
||||||
type: TYPES.ICE,
|
|
||||||
data: JSON.stringify(event.candidate),
|
|
||||||
});
|
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
pc.onconnectionstatechange = () => {
|
pc.onconnectionstatechange = () => {
|
||||||
if (pc.connectionState === 'connected') {
|
const state = pc.connectionState;
|
||||||
|
if (state === 'connected') {
|
||||||
setStatus(call, 'соединение установлено');
|
setStatus(call, 'соединение установлено');
|
||||||
|
void emitDebug(call, 'info', 'peer_connection_connected', `callId=${call.callId}`);
|
||||||
}
|
}
|
||||||
if (pc.connectionState === 'failed' || pc.connectionState === 'closed' || pc.connectionState === 'disconnected') {
|
if (state === 'failed' || state === 'closed' || state === 'disconnected') {
|
||||||
finishCall(call, `state=${pc.connectionState}`, false);
|
void emitDebug(call, 'warn', 'peer_connection_closed', `state=${state}`);
|
||||||
|
finishCall(call, `state=${state}`, false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,6 +158,7 @@ async function ensurePeerConnection(call) {
|
|||||||
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
|
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(call, `нет доступа к микрофону: ${e?.message || 'unknown'}`);
|
setStatus(call, `нет доступа к микрофону: ${e?.message || 'unknown'}`);
|
||||||
|
await emitDebug(call, 'warn', 'microphone_access_failed', toErrorText(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
pc.ontrack = (evt) => {
|
pc.ontrack = (evt) => {
|
||||||
@ -113,17 +172,6 @@ async function ensurePeerConnection(call) {
|
|||||||
return pc;
|
return pc;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendSignal(call, type, data = '') {
|
|
||||||
if (!call.remoteSessionId) return;
|
|
||||||
await authService.callSignalToSession({
|
|
||||||
toLogin: call.peerLogin,
|
|
||||||
targetSessionId: call.remoteSessionId,
|
|
||||||
callId: call.callId,
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onAccept(call) {
|
async function onAccept(call) {
|
||||||
cleanupTimers(call);
|
cleanupTimers(call);
|
||||||
const pc = await ensurePeerConnection(call);
|
const pc = await ensurePeerConnection(call);
|
||||||
@ -133,6 +181,72 @@ async function onAccept(call) {
|
|||||||
setStatus(call, 'отправлен offer');
|
setStatus(call, 'отправлен offer');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setCallDebugReporter(fn) {
|
||||||
|
debugReporter = typeof fn === 'function' ? fn : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startDebugConnectionAsResponder({ runId, callId, peerLogin, peerSessionId }) {
|
||||||
|
const cleanCallId = String(callId || '').trim();
|
||||||
|
const cleanPeerLogin = String(peerLogin || '').trim();
|
||||||
|
const cleanPeerSessionId = String(peerSessionId || '').trim();
|
||||||
|
if (!cleanCallId || !cleanPeerLogin || !cleanPeerSessionId) return;
|
||||||
|
|
||||||
|
let call = getCall(cleanCallId);
|
||||||
|
if (!call) {
|
||||||
|
call = {
|
||||||
|
callId: cleanCallId,
|
||||||
|
peerLogin: cleanPeerLogin,
|
||||||
|
direction: 'in',
|
||||||
|
state: 'accepted',
|
||||||
|
remoteSessionId: cleanPeerSessionId,
|
||||||
|
timers: {},
|
||||||
|
startedAtMs: nowMs(),
|
||||||
|
pc: null,
|
||||||
|
localStream: null,
|
||||||
|
debugMode: true,
|
||||||
|
debugRunId: String(runId || '').trim(),
|
||||||
|
debugRole: 'responder',
|
||||||
|
};
|
||||||
|
calls.set(cleanCallId, call);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeCallId = cleanCallId;
|
||||||
|
await emitDebug(call, 'info', 'debug_prepare_responder', `peerSessionId=${cleanPeerSessionId}`);
|
||||||
|
setStatus(call, 'debug: responder готов, ждём offer');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startDebugConnectionAsInitiator({ runId, callId, peerLogin, peerSessionId }) {
|
||||||
|
const cleanCallId = String(callId || '').trim();
|
||||||
|
const cleanPeerLogin = String(peerLogin || '').trim();
|
||||||
|
const cleanPeerSessionId = String(peerSessionId || '').trim();
|
||||||
|
if (!cleanCallId || !cleanPeerLogin || !cleanPeerSessionId) return;
|
||||||
|
|
||||||
|
const call = {
|
||||||
|
callId: cleanCallId,
|
||||||
|
peerLogin: cleanPeerLogin,
|
||||||
|
direction: 'out',
|
||||||
|
state: 'accepted',
|
||||||
|
remoteSessionId: cleanPeerSessionId,
|
||||||
|
timers: {},
|
||||||
|
startedAtMs: nowMs(),
|
||||||
|
pc: null,
|
||||||
|
localStream: null,
|
||||||
|
debugMode: true,
|
||||||
|
debugRunId: String(runId || '').trim(),
|
||||||
|
debugRole: 'initiator',
|
||||||
|
};
|
||||||
|
|
||||||
|
calls.set(cleanCallId, call);
|
||||||
|
activeCallId = cleanCallId;
|
||||||
|
await emitDebug(call, 'info', 'debug_start_initiator', `peerSessionId=${cleanPeerSessionId}`);
|
||||||
|
try {
|
||||||
|
await onAccept(call);
|
||||||
|
} catch (error) {
|
||||||
|
await emitDebug(call, 'error', 'debug_initiator_start_failed', toErrorText(error));
|
||||||
|
await finishCall(call, `debug start failed: ${toErrorText(error)}`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function startOutgoingCall(peerLogin) {
|
export async function startOutgoingCall(peerLogin) {
|
||||||
const cleanPeer = String(peerLogin || '').trim();
|
const cleanPeer = String(peerLogin || '').trim();
|
||||||
if (!cleanPeer) return;
|
if (!cleanPeer) return;
|
||||||
@ -153,6 +267,9 @@ export async function startOutgoingCall(peerLogin) {
|
|||||||
startedAtMs: nowMs(),
|
startedAtMs: nowMs(),
|
||||||
pc: null,
|
pc: null,
|
||||||
localStream: null,
|
localStream: null,
|
||||||
|
debugMode: false,
|
||||||
|
debugRunId: '',
|
||||||
|
debugRole: '',
|
||||||
};
|
};
|
||||||
calls.set(callId, call);
|
calls.set(callId, call);
|
||||||
activeCallId = callId;
|
activeCallId = callId;
|
||||||
@ -203,6 +320,9 @@ export async function handleIncomingCallInvite(evt) {
|
|||||||
startedAtMs: nowMs(),
|
startedAtMs: nowMs(),
|
||||||
pc: null,
|
pc: null,
|
||||||
localStream: null,
|
localStream: null,
|
||||||
|
debugMode: false,
|
||||||
|
debugRunId: '',
|
||||||
|
debugRole: '',
|
||||||
};
|
};
|
||||||
calls.set(callId, call);
|
calls.set(callId, call);
|
||||||
}
|
}
|
||||||
@ -273,25 +393,43 @@ export async function handleIncomingCallSignal(evt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === TYPES.OFFER) {
|
if (type === TYPES.OFFER) {
|
||||||
const pc = await ensurePeerConnection(call);
|
try {
|
||||||
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
const pc = await ensurePeerConnection(call);
|
||||||
const answer = await pc.createAnswer();
|
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
||||||
await pc.setLocalDescription(answer);
|
const answer = await pc.createAnswer();
|
||||||
await sendSignal(call, TYPES.ANSWER, JSON.stringify(answer));
|
await pc.setLocalDescription(answer);
|
||||||
setStatus(call, 'получен offer, отправлен answer');
|
await sendSignal(call, TYPES.ANSWER, JSON.stringify(answer));
|
||||||
|
setStatus(call, 'получен offer, отправлен answer');
|
||||||
|
await emitDebug(call, 'info', 'offer_processed', 'answer sent');
|
||||||
|
} catch (error) {
|
||||||
|
await emitDebug(call, 'error', 'offer_process_failed', toErrorText(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === TYPES.ANSWER) {
|
if (type === TYPES.ANSWER) {
|
||||||
const pc = await ensurePeerConnection(call);
|
try {
|
||||||
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
const pc = await ensurePeerConnection(call);
|
||||||
setStatus(call, 'получен answer');
|
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
||||||
|
setStatus(call, 'получен answer');
|
||||||
|
await emitDebug(call, 'info', 'answer_processed', 'remote description set');
|
||||||
|
} catch (error) {
|
||||||
|
await emitDebug(call, 'error', 'answer_process_failed', toErrorText(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === TYPES.ICE) {
|
if (type === TYPES.ICE) {
|
||||||
const pc = await ensurePeerConnection(call);
|
try {
|
||||||
await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(data)));
|
const pc = await ensurePeerConnection(call);
|
||||||
|
await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(data)));
|
||||||
|
await emitDebug(call, 'info', 'ice_processed', 'candidate added');
|
||||||
|
} catch (error) {
|
||||||
|
await emitDebug(call, 'error', 'ice_process_failed', toErrorText(error));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -145,6 +145,14 @@ public final class ActiveConnectionsRegistry {
|
|||||||
return (set == null) ? Set.of() : set; // CopyOnWriteArraySet можно отдавать как есть
|
return (set == null) ? Set.of() : set; // CopyOnWriteArraySet можно отдавать как есть
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Снимок всех активных контекстов на текущий момент.
|
||||||
|
*/
|
||||||
|
public Set<ConnectionContext> getAllConnectionsSnapshot() {
|
||||||
|
return Set.copyOf(bySessionId.values());
|
||||||
|
}
|
||||||
|
|
||||||
private static String toLoginKey(String login) {
|
private static String toLoginKey(String login) {
|
||||||
return login.trim().toLowerCase(Locale.ROOT);
|
return login.trim().toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,8 +80,10 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Reque
|
|||||||
// --- NEW: Ping ---
|
// --- NEW: Ping ---
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
|
import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_Handler;
|
import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.system.Net_ClientDebugLog_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_ClientErrorLog_Request;
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request;
|
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;
|
||||||
|
|
||||||
@ -143,7 +145,8 @@ public final class JsonHandlerRegistry {
|
|||||||
// --- system ---
|
// --- system ---
|
||||||
Map.entry("Ping", new Net_Ping_Handler()),
|
Map.entry("Ping", new Net_Ping_Handler()),
|
||||||
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()),
|
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()),
|
||||||
Map.entry("ClientErrorLog", new Net_ClientErrorLog_Handler())
|
Map.entry("ClientErrorLog", new Net_ClientErrorLog_Handler()),
|
||||||
|
Map.entry("ClientDebugLog", new Net_ClientDebugLog_Handler())
|
||||||
|
|
||||||
// --- subscriptions ---
|
// --- subscriptions ---
|
||||||
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
||||||
@ -195,7 +198,8 @@ public final class JsonHandlerRegistry {
|
|||||||
// --- system ---
|
// --- system ---
|
||||||
Map.entry("Ping", Net_Ping_Request.class),
|
Map.entry("Ping", Net_Ping_Request.class),
|
||||||
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class),
|
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class),
|
||||||
Map.entry("ClientErrorLog", Net_ClientErrorLog_Request.class)
|
Map.entry("ClientErrorLog", Net_ClientErrorLog_Request.class),
|
||||||
|
Map.entry("ClientDebugLog", Net_ClientDebugLog_Request.class)
|
||||||
);
|
);
|
||||||
|
|
||||||
private JsonHandlerRegistry() { }
|
private JsonHandlerRegistry() { }
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.system;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.debug.DebugRunLogBuffer;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
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.handlers.system.entyties.Net_ClientDebugLog_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
|
||||||
|
public class Net_ClientDebugLog_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_ClientDebugLog_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_ClientDebugLog_Request req = (Net_ClientDebugLog_Request) baseRequest;
|
||||||
|
String message = safe(req.getMessage());
|
||||||
|
if (message.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Поле message обязательно");
|
||||||
|
}
|
||||||
|
|
||||||
|
String runId = safe(req.getRunId());
|
||||||
|
String level = safe(req.getLevel());
|
||||||
|
String sessionId = safe(ctx != null ? ctx.getSessionId() : "");
|
||||||
|
String login = safe(ctx != null ? ctx.getLogin() : "");
|
||||||
|
String details = safe(req.getDetails());
|
||||||
|
|
||||||
|
DebugRunLogBuffer.getInstance().add(level, runId, "client", sessionId, login, message, details);
|
||||||
|
|
||||||
|
log.info("CLIENT_DEBUG runId={} level={} login={} sessionId={} message={} details={}",
|
||||||
|
runId, level, login, sessionId, message, details);
|
||||||
|
|
||||||
|
Net_ClientDebugLog_Response resp = new Net_ClientDebugLog_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setAccepted(true);
|
||||||
|
resp.setServerTs(System.currentTimeMillis());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safe(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.system.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_ClientDebugLog_Request extends Net_Request {
|
||||||
|
private String runId;
|
||||||
|
private String level;
|
||||||
|
private String message;
|
||||||
|
private String details;
|
||||||
|
|
||||||
|
public String getRunId() { return runId; }
|
||||||
|
public void setRunId(String runId) { this.runId = runId; }
|
||||||
|
|
||||||
|
public String getLevel() { return level; }
|
||||||
|
public void setLevel(String level) { this.level = level; }
|
||||||
|
|
||||||
|
public String getMessage() { return message; }
|
||||||
|
public void setMessage(String message) { this.message = message; }
|
||||||
|
|
||||||
|
public String getDetails() { return details; }
|
||||||
|
public void setDetails(String details) { this.details = details; }
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.system.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_ClientDebugLog_Response extends Net_Response {
|
||||||
|
private boolean accepted;
|
||||||
|
private long serverTs;
|
||||||
|
|
||||||
|
public boolean isAccepted() { return accepted; }
|
||||||
|
public void setAccepted(boolean accepted) { this.accepted = accepted; }
|
||||||
|
|
||||||
|
public long getServerTs() { return serverTs; }
|
||||||
|
public void setServerTs(long serverTs) { this.serverTs = serverTs; }
|
||||||
|
}
|
||||||
40
src/main/java/server/debug/DebugApiConfigurator.java
Normal file
40
src/main/java/server/debug/DebugApiConfigurator.java
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package server.debug;
|
||||||
|
|
||||||
|
import jakarta.servlet.Servlet;
|
||||||
|
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Регистрирует HTTP debug endpoints.
|
||||||
|
*/
|
||||||
|
public final class DebugApiConfigurator {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DebugApiConfigurator.class);
|
||||||
|
private static final String CFG_DEBUG_API_ENABLED = "debug.tempApi.enabled";
|
||||||
|
|
||||||
|
private DebugApiConfigurator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void register(ServletContextHandler context) {
|
||||||
|
boolean enabled = AppConfig.getInstance().getBoolean(CFG_DEBUG_API_ENABLED, false);
|
||||||
|
if (!enabled) {
|
||||||
|
log.warn("⚠️ Debug API отключены настройкой {}=false", CFG_DEBUG_API_ENABLED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugTokenProvider tokenProvider = DebugTokenProvider.loadFromProjectRoot();
|
||||||
|
|
||||||
|
addServlet(context, new DebugClientsServlet(tokenProvider), "/debug/ws/clients");
|
||||||
|
addServlet(context, new DebugConnectServlet(tokenProvider), "/debug/ws/connect");
|
||||||
|
addServlet(context, new DebugLogsServlet(tokenProvider), "/debug/ws/logs");
|
||||||
|
log.info("✅ Debug API включены настройкой {}=true", CFG_DEBUG_API_ENABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addServlet(ServletContextHandler context, Servlet servlet, String path) {
|
||||||
|
ServletHolder holder = new ServletHolder(servlet);
|
||||||
|
context.addServlet(holder, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/main/java/server/debug/DebugClientsServlet.java
Normal file
140
src/main/java/server/debug/DebugClientsServlet.java
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package server.debug;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import jakarta.servlet.http.HttpServlet;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
|
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /debug/ws/clients
|
||||||
|
*/
|
||||||
|
public class DebugClientsServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
private final DebugTokenProvider tokenProvider;
|
||||||
|
|
||||||
|
public DebugClientsServlet(DebugTokenProvider tokenProvider) {
|
||||||
|
this.tokenProvider = tokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||||
|
if (!tokenProvider.isEnabled()) {
|
||||||
|
writeError(resp, 503, "DEBUG_DISABLED", "Debug API отключен: .debug-token не найден или пуст");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tokenProvider.matchesBearerHeader(req.getHeader("Authorization"))) {
|
||||||
|
writeError(resp, 401, "UNAUTHORIZED", "Неверный Bearer token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ConnectionContext> contexts = ActiveConnectionsRegistry.getInstance().getAllConnectionsSnapshot();
|
||||||
|
List<ObjectNode> rows = new ArrayList<>();
|
||||||
|
for (ConnectionContext ctx : contexts) {
|
||||||
|
Session ws = ctx.getWsSession();
|
||||||
|
if (ws == null || !ws.isOpen()) continue;
|
||||||
|
|
||||||
|
ObjectNode row = MAPPER.createObjectNode();
|
||||||
|
String sessionId = safe(ctx.getSessionId());
|
||||||
|
row.put("sessionId", sessionId);
|
||||||
|
row.put("login", safe(ctx.getLogin()));
|
||||||
|
row.put("authStatus", ctx.getAuthenticationStatus());
|
||||||
|
row.put("wsOpen", ws.isOpen());
|
||||||
|
|
||||||
|
String remote = safeRemote(ws.getRemoteAddress());
|
||||||
|
row.put("remoteAddress", remote);
|
||||||
|
row.put("ip", extractIp(ws.getRemoteAddress()));
|
||||||
|
row.put("userAgent", safeUserAgent(ws));
|
||||||
|
|
||||||
|
ActiveSessionEntry active = null;
|
||||||
|
try {
|
||||||
|
if (!sessionId.isBlank()) {
|
||||||
|
active = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
row.put("clientInfoFromClient", active != null ? safe(active.getClientInfoFromClient()) : "");
|
||||||
|
row.put("clientInfoFromRequest", active != null ? safe(active.getClientInfoFromRequest()) : "");
|
||||||
|
row.put("userLanguage", active != null ? safe(active.getUserLanguage()) : "");
|
||||||
|
row.put("sessionCreatedAtMs", active != null ? active.getSessionCreatedAtMs() : 0L);
|
||||||
|
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.sort(Comparator.comparing(a -> a.path("sessionId").asText("")));
|
||||||
|
|
||||||
|
ArrayNode clients = MAPPER.createArrayNode();
|
||||||
|
rows.forEach(clients::add);
|
||||||
|
|
||||||
|
ObjectNode payload = MAPPER.createObjectNode();
|
||||||
|
payload.put("count", clients.size());
|
||||||
|
payload.set("clients", clients);
|
||||||
|
|
||||||
|
writeOk(resp, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeUserAgent(Session ws) {
|
||||||
|
try {
|
||||||
|
if (ws.getUpgradeRequest() == null) return "";
|
||||||
|
return safe(ws.getUpgradeRequest().getHeader("User-Agent"));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractIp(SocketAddress addr) {
|
||||||
|
if (addr instanceof InetSocketAddress inet) {
|
||||||
|
if (inet.getAddress() != null) {
|
||||||
|
return safe(inet.getAddress().getHostAddress());
|
||||||
|
}
|
||||||
|
return safe(inet.getHostString());
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeRemote(SocketAddress addr) {
|
||||||
|
return addr == null ? "" : safe(addr.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safe(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeOk(HttpServletResponse resp, ObjectNode payload) throws IOException {
|
||||||
|
ObjectNode root = MAPPER.createObjectNode();
|
||||||
|
root.put("ok", true);
|
||||||
|
root.set("payload", payload == null ? MAPPER.createObjectNode() : payload);
|
||||||
|
writeJson(resp, 200, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeError(HttpServletResponse resp, int status, String code, String message) throws IOException {
|
||||||
|
ObjectNode root = MAPPER.createObjectNode();
|
||||||
|
root.put("ok", false);
|
||||||
|
root.put("code", code);
|
||||||
|
root.put("message", message);
|
||||||
|
writeJson(resp, status, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeJson(HttpServletResponse resp, int status, ObjectNode root) throws IOException {
|
||||||
|
resp.setStatus(status);
|
||||||
|
resp.setCharacterEncoding("UTF-8");
|
||||||
|
resp.setContentType("application/json; charset=UTF-8");
|
||||||
|
resp.getWriter().write(MAPPER.writeValueAsString(root));
|
||||||
|
}
|
||||||
|
}
|
||||||
148
src/main/java/server/debug/DebugConnectServlet.java
Normal file
148
src/main/java/server/debug/DebugConnectServlet.java
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package server.debug;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import jakarta.servlet.http.HttpServlet;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.push.WsEventSender;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /debug/ws/connect
|
||||||
|
*/
|
||||||
|
public class DebugConnectServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
private final DebugTokenProvider tokenProvider;
|
||||||
|
|
||||||
|
public DebugConnectServlet(DebugTokenProvider tokenProvider) {
|
||||||
|
this.tokenProvider = tokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||||
|
if (!tokenProvider.isEnabled()) {
|
||||||
|
writeError(resp, 503, "DEBUG_DISABLED", "Debug API отключен: .debug-token не найден или пуст");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tokenProvider.matchesBearerHeader(req.getHeader("Authorization"))) {
|
||||||
|
writeError(resp, 401, "UNAUTHORIZED", "Неверный Bearer token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode body;
|
||||||
|
try {
|
||||||
|
body = MAPPER.readTree(req.getInputStream());
|
||||||
|
} catch (Exception e) {
|
||||||
|
writeError(resp, 400, "BAD_JSON", "Тело запроса должно быть JSON");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String initiatorSessionId = text(body, "initiatorSessionId");
|
||||||
|
String responderSessionId = text(body, "responderSessionId");
|
||||||
|
boolean clearDebugLog = body != null && body.path("clearDebugLog").asBoolean(false);
|
||||||
|
|
||||||
|
if (initiatorSessionId.isBlank() || responderSessionId.isBlank()) {
|
||||||
|
writeError(resp, 400, "BAD_FIELDS", "initiatorSessionId и responderSessionId обязательны");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (initiatorSessionId.equals(responderSessionId)) {
|
||||||
|
writeError(resp, 400, "BAD_FIELDS", "Нужны две разные sessionId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionContext initiator = ActiveConnectionsRegistry.getInstance().getBySessionId(initiatorSessionId);
|
||||||
|
ConnectionContext responder = ActiveConnectionsRegistry.getInstance().getBySessionId(responderSessionId);
|
||||||
|
|
||||||
|
if (initiator == null || initiator.getWsSession() == null || !initiator.getWsSession().isOpen()) {
|
||||||
|
writeError(resp, 404, "INITIATOR_NOT_FOUND", "Сессия initiator не найдена или неактивна");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (responder == null || responder.getWsSession() == null || !responder.getWsSession().isOpen()) {
|
||||||
|
writeError(resp, 404, "RESPONDER_NOT_FOUND", "Сессия responder не найдена или неактивна");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugRunLogBuffer logs = DebugRunLogBuffer.getInstance();
|
||||||
|
if (clearDebugLog) {
|
||||||
|
logs.clear();
|
||||||
|
}
|
||||||
|
String runId = logs.newRunId();
|
||||||
|
String callId = "debug-call-" + System.currentTimeMillis();
|
||||||
|
|
||||||
|
ObjectNode responderPayload = MAPPER.createObjectNode();
|
||||||
|
responderPayload.put("runId", runId);
|
||||||
|
responderPayload.put("callId", callId);
|
||||||
|
responderPayload.put("peerSessionId", initiatorSessionId);
|
||||||
|
responderPayload.put("peerLogin", safe(initiator.getLogin()));
|
||||||
|
responderPayload.put("role", "responder");
|
||||||
|
responderPayload.put("issuedAtMs", System.currentTimeMillis());
|
||||||
|
|
||||||
|
ObjectNode initiatorPayload = MAPPER.createObjectNode();
|
||||||
|
initiatorPayload.put("runId", runId);
|
||||||
|
initiatorPayload.put("callId", callId);
|
||||||
|
initiatorPayload.put("peerSessionId", responderSessionId);
|
||||||
|
initiatorPayload.put("peerLogin", safe(responder.getLogin()));
|
||||||
|
initiatorPayload.put("role", "initiator");
|
||||||
|
initiatorPayload.put("issuedAtMs", System.currentTimeMillis());
|
||||||
|
|
||||||
|
boolean responderOk = WsEventSender.sendEvent(responder, "DebugConnectPrepareResponder", "evt-" + callId + "-r", responderPayload);
|
||||||
|
boolean initiatorOk = WsEventSender.sendEvent(initiator, "DebugConnectStartInitiator", "evt-" + callId + "-i", initiatorPayload);
|
||||||
|
|
||||||
|
logs.add("info", runId, "debug-connect", initiatorSessionId, safe(initiator.getLogin()),
|
||||||
|
"Запуск debug-run", "initiator->" + responderSessionId + ", sentResponder=" + responderOk + ", sentInitiator=" + initiatorOk);
|
||||||
|
|
||||||
|
ObjectNode payload = MAPPER.createObjectNode();
|
||||||
|
payload.put("runId", runId);
|
||||||
|
payload.put("callId", callId);
|
||||||
|
payload.put("accepted", responderOk && initiatorOk);
|
||||||
|
payload.put("initiatorSessionId", initiatorSessionId);
|
||||||
|
payload.put("responderSessionId", responderSessionId);
|
||||||
|
payload.put("initiatorLogin", safe(initiator.getLogin()));
|
||||||
|
payload.put("responderLogin", safe(responder.getLogin()));
|
||||||
|
if (!safe(initiator.getLogin()).equalsIgnoreCase(safe(responder.getLogin()))) {
|
||||||
|
payload.put("mode", "cross-login");
|
||||||
|
} else {
|
||||||
|
payload.put("warning", "Сессии принадлежат одному login, рекомендован тест между разными login");
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOk(resp, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String text(JsonNode node, String field) {
|
||||||
|
if (node == null) return "";
|
||||||
|
return safe(node.path(field).asText(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safe(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeOk(HttpServletResponse resp, ObjectNode payload) throws IOException {
|
||||||
|
ObjectNode root = MAPPER.createObjectNode();
|
||||||
|
root.put("ok", true);
|
||||||
|
root.set("payload", payload == null ? MAPPER.createObjectNode() : payload);
|
||||||
|
writeJson(resp, 200, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeError(HttpServletResponse resp, int status, String code, String message) throws IOException {
|
||||||
|
ObjectNode root = MAPPER.createObjectNode();
|
||||||
|
root.put("ok", false);
|
||||||
|
root.put("code", code);
|
||||||
|
root.put("message", message);
|
||||||
|
writeJson(resp, status, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeJson(HttpServletResponse resp, int status, ObjectNode root) throws IOException {
|
||||||
|
resp.setStatus(status);
|
||||||
|
resp.setCharacterEncoding("UTF-8");
|
||||||
|
resp.setContentType("application/json; charset=UTF-8");
|
||||||
|
resp.getWriter().write(MAPPER.writeValueAsString(root));
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/main/java/server/debug/DebugLogsServlet.java
Normal file
100
src/main/java/server/debug/DebugLogsServlet.java
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package server.debug;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import jakarta.servlet.http.HttpServlet;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /debug/ws/logs?limit=200&runId=dbg-...
|
||||||
|
*/
|
||||||
|
public class DebugLogsServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
private final DebugTokenProvider tokenProvider;
|
||||||
|
|
||||||
|
public DebugLogsServlet(DebugTokenProvider tokenProvider) {
|
||||||
|
this.tokenProvider = tokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||||
|
if (!tokenProvider.isEnabled()) {
|
||||||
|
writeError(resp, 503, "DEBUG_DISABLED", "Debug API отключен: .debug-token не найден или пуст");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tokenProvider.matchesBearerHeader(req.getHeader("Authorization"))) {
|
||||||
|
writeError(resp, 401, "UNAUTHORIZED", "Неверный Bearer token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = parseLimit(req.getParameter("limit"));
|
||||||
|
String runId = safe(req.getParameter("runId"));
|
||||||
|
|
||||||
|
List<DebugRunLogBuffer.Entry> list = DebugRunLogBuffer.getInstance().tail(limit, runId);
|
||||||
|
|
||||||
|
ArrayNode rows = MAPPER.createArrayNode();
|
||||||
|
for (DebugRunLogBuffer.Entry e : list) {
|
||||||
|
ObjectNode row = MAPPER.createObjectNode();
|
||||||
|
row.put("ts", e.ts);
|
||||||
|
row.put("level", e.level);
|
||||||
|
row.put("runId", e.runId);
|
||||||
|
row.put("source", e.source);
|
||||||
|
row.put("sessionId", e.sessionId);
|
||||||
|
row.put("login", e.login);
|
||||||
|
row.put("message", e.message);
|
||||||
|
row.put("details", e.details);
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectNode payload = MAPPER.createObjectNode();
|
||||||
|
payload.put("count", rows.size());
|
||||||
|
payload.put("limit", limit);
|
||||||
|
payload.put("runIdFilter", runId);
|
||||||
|
payload.set("logs", rows);
|
||||||
|
|
||||||
|
writeOk(resp, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseLimit(String raw) {
|
||||||
|
try {
|
||||||
|
int n = Integer.parseInt(String.valueOf(raw));
|
||||||
|
if (n < 1) return 100;
|
||||||
|
return Math.min(n, 1000);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safe(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeOk(HttpServletResponse resp, ObjectNode payload) throws IOException {
|
||||||
|
ObjectNode root = MAPPER.createObjectNode();
|
||||||
|
root.put("ok", true);
|
||||||
|
root.set("payload", payload == null ? MAPPER.createObjectNode() : payload);
|
||||||
|
writeJson(resp, 200, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeError(HttpServletResponse resp, int status, String code, String message) throws IOException {
|
||||||
|
ObjectNode root = MAPPER.createObjectNode();
|
||||||
|
root.put("ok", false);
|
||||||
|
root.put("code", code);
|
||||||
|
root.put("message", message);
|
||||||
|
writeJson(resp, status, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeJson(HttpServletResponse resp, int status, ObjectNode root) throws IOException {
|
||||||
|
resp.setStatus(status);
|
||||||
|
resp.setCharacterEncoding("UTF-8");
|
||||||
|
resp.setContentType("application/json; charset=UTF-8");
|
||||||
|
resp.getWriter().write(MAPPER.writeValueAsString(root));
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/main/java/server/debug/DebugRunLogBuffer.java
Normal file
81
src/main/java/server/debug/DebugRunLogBuffer.java
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package server.debug;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Потокобезопасный in-memory буфер debug-логов.
|
||||||
|
*/
|
||||||
|
public final class DebugRunLogBuffer {
|
||||||
|
|
||||||
|
public static final class Entry {
|
||||||
|
public long ts;
|
||||||
|
public String level;
|
||||||
|
public String runId;
|
||||||
|
public String source;
|
||||||
|
public String sessionId;
|
||||||
|
public String login;
|
||||||
|
public String message;
|
||||||
|
public String details;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final DebugRunLogBuffer INSTANCE = new DebugRunLogBuffer(5_000);
|
||||||
|
|
||||||
|
public static DebugRunLogBuffer getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int capacity;
|
||||||
|
private final Deque<Entry> entries = new ArrayDeque<>();
|
||||||
|
|
||||||
|
private DebugRunLogBuffer(int capacity) {
|
||||||
|
this.capacity = Math.max(100, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String newRunId() {
|
||||||
|
return "dbg-" + UUID.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void clear() {
|
||||||
|
entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void add(String level, String runId, String source, String sessionId, String login, String message, String details) {
|
||||||
|
Entry e = new Entry();
|
||||||
|
e.ts = System.currentTimeMillis();
|
||||||
|
e.level = safe(level, "info");
|
||||||
|
e.runId = safe(runId, "");
|
||||||
|
e.source = safe(source, "server");
|
||||||
|
e.sessionId = safe(sessionId, "");
|
||||||
|
e.login = safe(login, "");
|
||||||
|
e.message = safe(message, "");
|
||||||
|
e.details = safe(details, "");
|
||||||
|
|
||||||
|
entries.addLast(e);
|
||||||
|
while (entries.size() > capacity) {
|
||||||
|
entries.removeFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<Entry> tail(int limit, String runIdFilter) {
|
||||||
|
int capped = Math.max(1, Math.min(limit, 1000));
|
||||||
|
String filter = safe(runIdFilter, "");
|
||||||
|
|
||||||
|
List<Entry> out = new ArrayList<>(capped);
|
||||||
|
Object[] arr = entries.toArray();
|
||||||
|
for (int i = arr.length - 1; i >= 0 && out.size() < capped; i--) {
|
||||||
|
Entry e = (Entry) arr[i];
|
||||||
|
if (!filter.isBlank() && !filter.equals(e.runId)) continue;
|
||||||
|
out.add(e);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safe(String value, String fallback) {
|
||||||
|
String s = value == null ? "" : value.trim();
|
||||||
|
return s.isBlank() ? fallback : s;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/main/java/server/debug/DebugTokenProvider.java
Normal file
54
src/main/java/server/debug/DebugTokenProvider.java
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package server.debug;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает debug token из файла .debug-token в корне проекта.
|
||||||
|
*/
|
||||||
|
public final class DebugTokenProvider {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DebugTokenProvider.class);
|
||||||
|
private static final String TOKEN_FILE_NAME = ".debug-token";
|
||||||
|
|
||||||
|
private final String token;
|
||||||
|
|
||||||
|
private DebugTokenProvider(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DebugTokenProvider loadFromProjectRoot() {
|
||||||
|
Path tokenPath = Path.of(TOKEN_FILE_NAME).toAbsolutePath().normalize();
|
||||||
|
String loaded = "";
|
||||||
|
try {
|
||||||
|
if (Files.exists(tokenPath)) {
|
||||||
|
loaded = Files.readString(tokenPath, StandardCharsets.UTF_8).trim();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Не удалось прочитать debug token из {}", tokenPath, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaded.isBlank()) {
|
||||||
|
log.warn("⚠️ Debug API отключены: файл {} отсутствует или пуст.", tokenPath);
|
||||||
|
} else {
|
||||||
|
log.info("✅ Debug API включены, token файл найден: {}", tokenPath);
|
||||||
|
}
|
||||||
|
return new DebugTokenProvider(loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return token != null && !token.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matchesBearerHeader(String authorizationHeader) {
|
||||||
|
if (!isEnabled()) return false;
|
||||||
|
if (authorizationHeader == null || authorizationHeader.isBlank()) return false;
|
||||||
|
if (!authorizationHeader.startsWith("Bearer ")) return false;
|
||||||
|
String actual = authorizationHeader.substring("Bearer ".length()).trim();
|
||||||
|
return token.equals(actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
|
|||||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.debug.DebugApiConfigurator;
|
||||||
import utils.config.AppConfig;
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@ -58,6 +59,9 @@ public final class WsServer {
|
|||||||
context.setContextPath("/");
|
context.setContextPath("/");
|
||||||
server.setHandler(context);
|
server.setHandler(context);
|
||||||
|
|
||||||
|
// HTTP debug API
|
||||||
|
DebugApiConfigurator.register(context);
|
||||||
|
|
||||||
// Инициализация контейнера WebSocket
|
// Инициализация контейнера WebSocket
|
||||||
JettyWebSocketServletContainerInitializer.configure(context, (servletContext, wsContainer) -> {
|
JettyWebSocketServletContainerInitializer.configure(context, (servletContext, wsContainer) -> {
|
||||||
// Таймаут простоя соединения (Jetty 11 синтаксис)
|
// Таймаут простоя соединения (Jetty 11 синтаксис)
|
||||||
|
|||||||
@ -17,3 +17,11 @@ server.info.extraInfo=
|
|||||||
webpush.vapid.public=BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI
|
webpush.vapid.public=BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI
|
||||||
webpush.vapid.private=3hCt7XxTvLzuoxinjT5QcKRQEBnGZHXn8ZilU31RPNE
|
webpush.vapid.private=3hCt7XxTvLzuoxinjT5QcKRQEBnGZHXn8ZilU31RPNE
|
||||||
webpush.vapid.subject=mailto:admin@shine.local
|
webpush.vapid.subject=mailto:admin@shine.local
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Временные debug HTTP API для тестирования соединений
|
||||||
|
# true - endpoint'ы /debug/ws/* включены (только при наличии .debug-token)
|
||||||
|
# false - endpoint'ы /debug/ws/* полностью отключены
|
||||||
|
# Если параметр отсутствует, по умолчанию считается false
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
debug.tempApi.enabled=true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user