Files
XC_VM/docs/ru/development/http-request-handling.md
T
Divarion-D 76844fef11 docs: restructure, fix PSR-4 drift, and unify en/ru
Overhaul the Docsify documentation (English + Russian) so it matches the current
codebase and follows one consistent pattern.

Content accuracy (post-migration):
- Rewrite development/autoloader.md to PSR-4 / Composer (the old XC_Autoloader
  scanner, igbinary tmp/cache/autoload_map and registerDirectories are gone).
- PascalCase every source path (src/core -> src/Core, domain/Stream, cli/Commands,
  public/Controllers, Infrastructure/Redis, ...) across all docs.
- Replace the removed autoload.php references with vendor/autoload.php
  (build_system, bootstrap-contexts, error-handling, modules).
- ssl-generation: note that the installer now auto-generates a unique self-signed
  certificate before Nginx starts.

Common pattern (Clean & uniform):
- Strip emoji from headings; remove the in-page Navigation blocks (the Docsify
  sidebar already provides navigation).
- One H1 + intro per doc; uniform "Related files" / "Связанные файлы" section,
  added to the code-centric docs that lacked it.

Structure:
- Remove the empty stray docs/api/; move updates_checklist.md into builds/;
  link the previously-orphaned ucs-integration.md.
- Regroup the sidebars (split the oversized guides group into Developer Guides /
  Security & Access / Integrations; fold builds into Build & Release).

Augment:
- dev-workflow: Local Setup (make dev-tools) + Quality Checks (phpstan, cs, gates).
- build_system: Composer Dependencies section (committed prod-only vendor,
  committed lock, dev tools via composer install, no build-time vendor step).

en/ru parity:
- Apply the same structure, fixes and pattern to docs/ru/ (translated), including
  a new Russian ucs-integration.md. The en and ru file sets are now identical.
2026-06-26 15:56:15 +03:00

407 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Обработка HTTP-запросов
Этот документ описывает, как обрабатываются HTTP-запросы в XC_VM, охватывая полный жизненный цикл — от первичного входа до маршрутизации и диспетчеризации. Существует несколько runtime-путей в зависимости от типа запроса.
---
## Обзор
HTTP-уровень состоит из следующих основных компонентов:
| Компонент | Файл | Роль |
| --- | --- | --- |
| `RequestGuard` | `src/Core/Http/RequestGuard.php` | Защита до маршрутизации: защита от флуда, проверка хоста, инициализация логгера |
| `InputValidator` | `src/Core/Validation/InputValidator.php` | Санитизация ввода (cleanGlobals, parseIncomingRecursively) |
| `RequestManager` | `src/Core/Http/RequestManager.php` | Статический фасад, хранящий объединённые данные GET+POST |
| `Request` | `src/Core/Http/Request.php` | Объектно-ориентированная обёртка запроса (существует, но не используется в основном продакшен-потоке) |
| `Router` | `src/Core/Http/Router.php` | Регистрация и диспетчеризация маршрутов страниц и API |
| `Response` | `src/Core/Http/Response.php` | Статические хелперы для ответов (JSON, redirect, CORS и т.д.) |
| `LegacyInitializer` | `src/Core/Init/LegacyInitializer.php` | Legacy-bootstrap, который связывает санитизацию с `RequestManager` |
| `StreamingRequestBootstrap` | `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php` | Лёгкий bootstrap для стриминговых endpoint'ов |
---
## Поток запроса: страницы Admin/Panel
Точка входа: `src/Public/index.php`
```text
nginx -> Public/index.php
-> разбор URL (scope + pageName)
-> XC_Bootstrap::boot(CONTEXT_ADMIN)
-> floodProtection() (блокировка забаненных IP)
-> hostVerification() (проверка allowed_domains)
-> initSession()
-> initDatabase()
-> initLegacyCore()
-> LegacyInitializer::initCore()
-> InputValidator::cleanGlobals($_GET, $_POST, $_SESSION, $_COOKIE)
-> InputValidator::parseIncomingRecursively($_GET) -> $rInput
-> InputValidator::parseIncomingRecursively($_POST, $rInput) -> RequestManager::set()
-> initRedis()
-> initAdminAPI()
-> initTranslator()
-> Загрузка маршрутов из src/Public/routes/{scope}.php
-> Загрузка маршрутов из src/Public/routes/api.php
-> ModuleLoader::bootAll() (scope admin/reseller, с детекцией коллизий)
-> Router::dispatchApi($action) (проверяется первым для страницы "api")
-> Router::dispatch($pageName, $method)
-> Обработчик контроллера
```
### Ключевая деталь: санитизация ввода
Продакшен-поток admin НЕ использует `Request::capture()`. Вместо этого обработкой ввода управляет `LegacyInitializer::initCore()`:
1. `InputValidator::cleanGlobals()` вызывается на `$_GET`, `$_POST`, `$_SESSION` и `$_COOKIE` in-place, удаляя NULL-байты, последовательности path traversal (`../`) и RTL override-символы.
2. `InputValidator::parseIncomingRecursively()` санитизирует ключи и значения (HTML-сущности, теги script, разделители комментариев, переводы строк) и возвращает чистый массив.
3. Результат (GET, объединённый с POST, причём POST имеет приоритет) сохраняется через `RequestManager::set()`.
По всему коду данные запроса читаются через `RequestManager::get($key)` и `RequestManager::getAll()`, а не через объект `Request`.
---
## Поток запроса: REST API
Точка входа: `src/Public/index.php` (срабатывает до Router'а)
Когда `XC_SCOPE` равно `includes/api/admin` или `includes/api/reseller`:
```text
nginx -> Public/index.php
-> XC_Bootstrap::boot(CONTEXT_ADMIN)
-> new AdminApiController() или new ResellerRestApiController()
-> $controller->index()
-> exit
```
Этот путь полностью обходит Router.
---
## Поток запроса: стриминг
Точка входа: endpoint'ы `www/stream/*.php` или `Public/index.php` для `player_api`
```text
nginx -> StreamingRequestBootstrap::init($filename)
-> Загрузка кодов ошибок, путей, конфигурации, бинарников
-> Защита от флуда (проверка файла block_{IP})
-> Загрузка настроек из файлового кэша
-> Проверка хоста
-> Инициализация логгера
-> StreamingBootstrap::bootstrap($filename, $settings)
-> LegacyInitializer::initStreaming()
-> Request::cleanGlobals() на суперглобалах
-> Request::parseIncomingRecursively() -> $GLOBALS['rRequest']
-> RequestManager::set($GLOBALS['rRequest'])
```
Стриминговый путь намеренно лёгкий. Он не загружает Router, EventDispatcher, Translator или полный ServiceContainer. Диспетчеризации маршрутов нет; у каждого стримингового endpoint'а — собственная точка входа.
---
## `RequestGuard`
Файл: `src/Core/Http/RequestGuard.php`
Процедурный guard-скрипт, подключаемый рано в legacy-bootstrap. Выполняется только для HTTP-запросов (пропускается при наличии `$_SERVER['argc']`, что указывает на CLI).
### Обязанности
1. **Защита от флуда** — если файл `FLOOD_TMP_PATH/block_{IP}` существует, запрос отклоняется с HTTP 403.
2. **Загрузка кэша настроек** — читает `$rSettings` из igbinary-сериализованного файлового кэша по пути `CACHE_TMP_PATH/settings`.
3. **Проверка хоста** — когда `$rSettings['verify_host']` равно true, проверяет, что `HOST` присутствует в кэшированном списке `allowed_domains`. Исключения: имя хоста `xc_vm` и любой валидный IP-адрес всегда разрешены.
4. **Флаг отображения ошибок** — устанавливает константу `PHP_ERRORS` из `$rSettings['debug_show_errors']`.
5. **Инициализация логгера** — вызывает `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`.
Примечание: В современном bootstrap (`XC_Bootstrap::boot()`) эти обязанности выполняют методы `floodProtection()` и `hostVerification()` напрямую, а не через подключение `RequestGuard.php`.
---
## `InputValidator`
Файл: `src/Core/Validation/InputValidator.php`
Предоставляет статические методы для санитизации ввода и валидации на уровне действий.
### Методы санитизации
| Метод | Описание |
| --- | --- |
| `cleanGlobals(&$data, $iteration)` | In-place удаление NULL-байтов, path traversal (`../`) и RTL override-символов. Максимум 10 уровней рекурсии. |
| `parseIncomingRecursively(&$data, $input, $iteration)` | Возвращает новый санитизированный массив. Очищает и ключи, и значения. Максимум 20 уровней рекурсии. |
| `parseCleanKey($key)` | Удаляет двойные точки, паттерны `__wrapped__`, применяет `htmlspecialchars(urldecode())`. |
| `parseCleanValue($value)` | Удаляет теги `<script`, HTML-комментарии, нормализует переводы строк, обрабатывает entity-кодирование. |
### Валидация действий
| Метод | Описание |
| --- | --- |
| `validate($action, $data)` | Возвращает `true`/`false` — соответствуют ли `$data` минимальным требованиям для заданного API-действия. |
| `validateOrFail($action, $data)` | Возвращает `null`, если валидно, или `['status' => STATUS_INVALID_INPUT, 'data' => $data]`, если нет. |
| `confirmIDs($ids)` | Фильтрует массив, оставляя только положительные целочисленные ID. |
---
## `RequestManager`
Файл: `src/Core/Http/RequestManager.php`
Статический фасад, хранящий объединённые данные GET+POST. Это основной паттерн доступа к данным запроса, используемый по всему коду.
### Как данные туда попадают
`LegacyInitializer::initCore()` вызывает:
```php
$rInput = InputValidator::parseIncomingRecursively($_GET, array());
RequestManager::set(InputValidator::parseIncomingRecursively($_POST, $rInput));
```
POST-параметры переопределяют GET-параметры с тем же ключом (POST накладывается поверх GET).
### API
| Метод | Описание |
| --- | --- |
| `set(array $request)` | Сохранить весь массив данных запроса. |
| `getAll()` | Получить все сохранённые данные запроса. |
| `get(string $key, $default = null)` | Получить одно значение по ключу. |
| `update(string $key, $value)` | Обновить один ключ в сохранённых данных. |
### Использование
```php
// Чтение параметра запроса
$streamId = RequestManager::get('stream_id');
// Чтение всех параметров
$allParams = RequestManager::getAll();
// Обновление значения (редко, используется некоторыми legacy-обработчиками)
RequestManager::update('status', 'active');
```
---
## `Request`
Файл: `src/Core/Http/Request.php`
Объектно-ориентированная обёртка запроса. Содержит статическую factory `capture()` и instance-методы для доступа к санитизированному вводу. Хотя класс существует и полностью функционален, основной продакшен-поток вместо этого использует `InputValidator` + `RequestManager`. Статические методы санитизации класса `Request` (`cleanGlobals`, `parseIncomingRecursively`) используются в `LegacyInitializer::initStreaming()` для обратной совместимости.
### Создание
```php
// Статическая factory (singleton, не используется в продакшен-потоке admin)
$request = Request::capture();
// Прямое создание
$request = new Request($_GET, $_POST, $_SERVER, $_COOKIE);
```
### Instance-методы
| Метод | Сигнатура | Описание |
| --- | --- | --- |
| `input` | `input($key, $default = null)` | Получить из объединённого ввода (POST имеет приоритет над GET) |
| `get` | `get($key = null, $default = null)` | Получить из query string (`$_GET`). Ключ `null` возвращает всё. |
| `post` | `post($key = null, $default = null)` | Получить из POST-данных. Ключ `null` возвращает всё. |
| `all` | `all()` | Все объединённые данные ввода |
| `has` | `has($key)` | Проверить, существует ли ключ в объединённом вводе |
| `getInt` | `getInt($key, $default = 0)` | Получить значение как integer |
| `getBool` | `getBool($key, $default = false)` | Получить значение как boolean (через `filter_var`) |
| `server` | `server($key, $default = null)` | Получить значение из `$_SERVER` |
| `cookie` | `cookie($key, $default = null)` | Получить значение cookie |
| `method` | `method()` | HTTP-метод (GET, POST и т.д.) |
| `isPost` | `isPost()` | Проверить, является ли метод POST |
| `isAjax` | `isAjax()` | Проверка `X-Requested-With: XMLHttpRequest` |
| `ip` | `ip()` | IP клиента (проверяет `X-Forwarded-For`, `X-Real-IP`, `REMOTE_ADDR`) |
| `uri` | `uri()` | URI запроса |
| `userAgent` | `userAgent()` | Заголовок User-Agent |
| `host` | `host()` | Заголовок Host (с fallback на `SERVER_NAME`) |
| `rawBody` | `rawBody()` | Сырое тело POST из `php://input` |
| `json` | `json($assoc = true)` | JSON-декодированное тело POST |
### Статические методы санитизации (обратная совместимость)
Зеркалят `InputValidator` и используются на пути инициализации стриминга:
| Метод | Описание |
| --- | --- |
| `cleanGlobals(&$data, $iteration)` | То же, что `InputValidator::cleanGlobals()` |
| `parseIncomingRecursively(&$data, $input, $iteration)` | То же, что `InputValidator::parseIncomingRecursively()` |
| `parseCleanKey($key)` | То же, что `InputValidator::parseCleanKey()` |
| `parseCleanValue($value)` | То же, что `InputValidator::parseCleanValue()` |
---
## `Router`
Файл: `src/Core/Http/Router.php`
Singleton-роутер для диспетчеризации страниц и API. Заменяет legacy-паттерн `switch($rAction)`.
### Регистрация маршрутов
| Метод | Сигнатура | Описание |
| --- | --- | --- |
| `get` | `get($route, $handler, $options = [])` | Зарегистрировать GET-маршрут страницы |
| `post` | `post($route, $handler, $options = [])` | Зарегистрировать POST-маршрут формы |
| `any` | `any($route, $handler, $options = [])` | Зарегистрировать одновременно GET и POST для одного маршрута |
| `api` | `api($action, $handler, $options = [])` | Зарегистрировать API-маршрут (JSON, диспетчеризируется по имени action) |
| `group` | `group($prefix, $callback, $options = [])` | Сгруппировать маршруты под общим префиксом с общим middleware/правами |
Параметр `$handler` принимает:
- `[ClassName::class, 'method']` — создаётся через ServiceContainer (с fallback на `new`)
- Closure или callable
- `[object, 'method']`
Массив `$options` поддерживает:
- `'permission' => ['type', 'key']` — проверяется через `Authorization::check()` перед запуском обработчика
- `'middleware' => [callable, ...]` — массив callables, выполняемых после проверки прав, перед обработчиком
### Примеры маршрутов
```php
$router = Router::getInstance();
// Простые маршруты страниц
$router->get('streams', [StreamController::class, 'index']);
$router->post('stream/save', [StreamController::class, 'save']);
// API-маршрут (JSON)
$router->api('deleteStream', [StreamController::class, 'apiDelete']);
// Сгруппированные маршруты с middleware и правами
$router->group('watch', function (Router $r) {
$r->get('', [WatchController::class, 'index']);
$r->get('add', [WatchController::class, 'add']);
$r->post('settings', [WatchController::class, 'saveSettings']);
$r->api('enable', [WatchController::class, 'apiEnable']);
}, [
'permission' => ['admin', 'watch'],
'middleware' => [$authCheck],
]);
```
### Нормализация маршрутов
Роутер нормализует legacy-имена страниц, преобразуя символы подчёркивания в слэши:
| Вход | Нормализованное |
| --- | --- |
| `watch` | `watch` |
| `watch_add` | `watch/add` |
| `settings_watch` | `settings/watch` |
| `plex_add.php` | `plex/add` |
Эта нормализация применяется как при регистрации (`buildRoute`), так и при диспетчеризации (`normalizePage`), поэтому маршруты, зарегистрированные как `watch/add`, совпадают с именами страниц вроде `watch_add`.
### Диспетчеризация
```php
// Диспетчеризация страницы (вызывается из Public/index.php)
$router->dispatch($pageName, $method); // возвращает true при совпадении
// Диспетчеризация API (вызывается для параметра action=)
$router->dispatchApi($action); // возвращает true при совпадении
```
#### Порядок выполнения `dispatch($page, $method)`
1. Нормализовать `$page` (подчёркивания → слэши, удалить `.php`).
2. Найти в POST-маршрутах (если метод POST) или GET-маршрутах. Если POST-маршрут не найден, идёт fallback на GET-маршруты.
3. **Проверка прав** через `checkPermission()`. При отказе вызывается `denyAccess()` (редирект или 403).
4. **Выполнение middleware**. Каждый callable из массива `middleware` вызывается. Если любой возвращает `false`, выполнение останавливается.
5. **Вызов обработчика** через `callHandler()`.
#### Порядок выполнения `dispatchApi($action)`
1. Найти в API-маршрутах по имени действия.
2. **Проверка прав**. При отказе выводится `{"result": false}` и происходит выход.
3. **Вызов обработчика**. Middleware не выполняется.
Важно: `dispatchApi()` НЕ запускает middleware. Это намеренное отличие от диспетчеризации страниц.
### Регистрация модульных маршрутов
Модули регистрируют маршруты через `ModuleInterface::registerRoutes()`. Роутер поддерживает безопасный режим регистрации, предотвращающий перезапись модулями core-маршрутов:
```php
$router->beginModuleRegistration();
// Маршруты модулей регистрируются здесь — дубликаты молча пропускаются
$moduleLoader->bootAll($container, $router);
$router->endModuleRegistration();
// Проверка коллизий (логируется в режиме development)
$collisions = $router->drainRouteCollisions();
```
В режиме регистрации модулей (`preserveExistingRoutes = true`), если модуль пытается зарегистрировать уже существующий маршрут, существующий сохраняется, а коллизия фиксируется. `drainRouteCollisions()` возвращает и очищает накопленные коллизии в виде массива `['type' => 'get'|'post'|'api', 'key' => 'route/path']`.
### Интроспекция
| Метод | Описание |
| --- | --- |
| `hasRoute($page)` | Проверить, существует ли маршрут страницы (GET или POST) |
| `hasApiRoute($action)` | Проверить, существует ли API-маршрут |
| `getRoutes()` | Вернуть все зарегистрированные ключи маршрутов как `['get' => [...], 'post' => [...], 'api' => [...]]` |
---
## `Response`
Файл: `src/Core/Http/Response.php`
Статический хелпер для отправки HTTP-ответов. Заменяет разрозненные паттерны `header()` + `echo` + `exit()`.
| Метод | Сигнатура | Описание |
| --- | --- | --- |
| `json` | `json($data, $statusCode = 200, $options = 0)` | Отправить JSON-ответ и выйти |
| `jsonError` | `jsonError($message, $statusCode = 400, $extra = [])` | Отправить JSON-ошибку и выйти |
| `redirect` | `redirect($url, $statusCode = 302)` | Отправить редирект и выйти |
| `notFound` | `notFound($message = 'Not Found')` | Отправить 404 и выйти |
| `header` | `header($name, $value)` | Установить один заголовок ответа |
| `cors` | `cors()` | Установить CORS-заголовки (`Access-Control-Allow-Origin: *`) |
| `noCache` | `noCache()` | Установить no-cache заголовки (используется для HLS-плейлистов) |
| `raw` | `raw($content, $contentType, $statusCode)` | Отправить сырой контент с указанным content type и выйти |
| `empty` | `empty($statusCode = 204)` | Отправить пустой ответ и выйти |
---
## Контексты bootstrap
`XC_Bootstrap::boot($context)` обеспечивает контекстно-зависимую инициализацию. Каждый контекст надстраивается над предыдущим:
| Контекст | Что инициализируется |
| --- | --- |
| `CONTEXT_MINIMAL` | Autoload + константы + конфигурация + Logger. Без подключения к БД. |
| `CONTEXT_CLI` | + База данных + `LegacyInitializer::initCore()` (санитизация ввода, настройки, пути FFmpeg). Опционально Redis. |
| `CONTEXT_STREAM` | + Только база данных (лёгкий, без `LegacyInitializer`). Стриминговые endpoint'ы используют `StreamingRequestBootstrap`. |
| `CONTEXT_ADMIN` | + Сессия + база данных + `LegacyInitializer::initCore()` + Redis + Admin API + Translator + admin globals. Полная инициализация. |
Все HTTP-контексты (не CLI) также выполняют защиту от флуда и проверку хоста перед контекстно-зависимой инициализацией.
---
## Связанные файлы
| Файл | Назначение |
| --- | --- |
| `src/Core/Http/RequestGuard.php` | Защита до маршрутизации и инициализация логгера (legacy include) |
| `src/Core/Http/Request.php` | OOP-обёртка запроса с методами санитизации |
| `src/Core/Http/Router.php` | Регистрация и диспетчеризация маршрутов |
| `src/Core/Http/RequestManager.php` | Статический фасад данных запроса (основной паттерн доступа) |
| `src/Core/Http/Response.php` | Хелперы вывода ответов |
| `src/Core/Validation/InputValidator.php` | Санитизация ввода и валидация действий |
| `src/Core/Init/LegacyInitializer.php` | Legacy core-инициализация (связывает санитизацию с RequestManager) |
| `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php` | Лёгкий bootstrap стримингового endpoint'а |
| `src/Streaming/StreamingBootstrap.php` | Подключение к БД и legacy-инициализация стриминга |
| `src/bootstrap.php` | Унифицированный bootstrap (класс `XC_Bootstrap`) |
| `src/Public/index.php` | Front-контроллер для admin/reseller/player/API |
| `src/Public/routes/admin.php` | Определения маршрутов admin-страниц |
| `src/Public/routes/reseller.php` | Определения маршрутов reseller-страниц |
| `src/Public/routes/player.php` | Определения маршрутов player-страниц |