HTTP Request Handling¶
This document describes how HTTP requests are processed in XC_VM, covering the full lifecycle from initial entry through routing and dispatch. There are multiple runtime paths depending on the request type.
Overview¶
The HTTP layer is built from these core components:
| Component | File | Role |
|---|---|---|
RequestGuard |
src/Core/Http/RequestGuard.php |
Pre-routing safety: flood protection, host verification, logger init |
InputValidator |
src/Core/Validation/InputValidator.php |
Input sanitization (cleanGlobals, parseIncomingRecursively) |
RequestManager |
src/Core/Http/RequestManager.php |
Static facade storing merged GET+POST request data |
Request |
src/Core/Http/Request.php |
Object-oriented request wrapper (exists but not used in the primary production flow) |
Router |
src/Core/Http/Router.php |
Page and API route registration and dispatch |
Response |
src/Core/Http/Response.php |
Static response helpers (JSON, redirect, CORS, etc.) |
LegacyInitializer |
src/Core/Init/LegacyInitializer.php |
Legacy bootstrap that wires sanitization into RequestManager |
StreamingRequestBootstrap |
src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php |
Lightweight bootstrap for streaming endpoints |
Request Flow: Admin/Panel Pages¶
Entry point: src/Public/index.php
nginx -> Public/index.php
-> URL parsing (scope + pageName)
-> XC_Bootstrap::boot(BootContext::Admin)
-> floodProtection() (block banned IPs)
-> hostVerification() (check 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()
-> Load routes from src/Public/routes/{scope}.php
-> Load routes from src/Public/routes/api.php
-> ModuleLoader::bootAll() (admin/reseller scope, with collision detection)
-> Router::dispatchApi($action) (checked first for "api" page)
-> Router::dispatch($pageName, $method)
-> Controller handler
Key detail: input sanitization¶
The production admin flow does NOT use Request::capture(). Instead, LegacyInitializer::initCore() drives input processing:
InputValidator::cleanGlobals()is called on$_GET,$_POST,$_SESSION, and$_COOKIEin place, stripping NULL bytes, path traversal sequences (../), and RTL override characters.InputValidator::parseIncomingRecursively()sanitizes keys and values (HTML entities, script tags, comment delimiters, line endings) and returns a clean array.- The result (GET merged with POST, POST taking priority) is stored via
RequestManager::set().
Throughout the codebase, request data is accessed via RequestManager::get($key) and RequestManager::getAll(), not through the Request object.
Request Flow: REST API¶
Entry point: src/Public/index.php (short-circuits before Router)
When XC_SCOPE is includes/api/admin or includes/api/reseller:
nginx -> Public/index.php
-> XC_Bootstrap::boot(BootContext::Admin)
-> new AdminApiController() or new ResellerRestApiController()
-> $controller->index()
-> exit
This path bypasses the Router entirely.
Request Flow: Streaming¶
Entry point: www/stream/*.php endpoints, or Public/index.php for player_api
nginx -> StreamingRequestBootstrap::init($filename)
-> Load error codes, paths, config, binaries
-> Flood protection (check block_{IP} file)
-> Load settings from file cache
-> Host verification
-> Logger init
-> StreamingBootstrap::bootstrap($filename, $settings)
-> LegacyInitializer::initStreaming()
-> Request::cleanGlobals() on superglobals
-> Request::parseIncomingRecursively() -> $GLOBALS['rRequest']
-> RequestManager::set($GLOBALS['rRequest'])
The streaming path is deliberately lightweight. It does not load the Router, EventDispatcher, Translator, or full ServiceContainer. There is no route dispatch; each streaming endpoint has a dedicated entry point.
RequestGuard¶
File: src/Core/Http/RequestGuard.php
A procedural guard script included early in the legacy bootstrap. Runs only for HTTP requests (skipped when $_SERVER['argc'] is set, indicating CLI).
Responsibilities¶
- Flood protection -- If the file
FLOOD_TMP_PATH/block_{IP}exists, the request is rejected with HTTP 403. - Settings cache load -- Reads
$rSettingsfrom the igbinary-serialized file cache atCACHE_TMP_PATH/settings. - Host verification -- When
$rSettings['verify_host']is true, checks thatHOSTappears in the cachedallowed_domainslist. Exceptions: the hostnamexc_vmand any valid IP address are always allowed. - Error display flag -- Sets the
PHP_ERRORSconstant from$rSettings['debug_show_errors']. - Logger initialization -- Calls
Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log').
Note: In the modern bootstrap (XC_Bootstrap::boot()), these responsibilities are handled by floodProtection() and hostVerification() methods directly, not by including RequestGuard.php.
InputValidator¶
File: src/Core/Validation/InputValidator.php
Provides static methods for input sanitization and action-level validation.
Sanitization methods¶
| Method | Description |
|---|---|
cleanGlobals(&$data, $iteration) |
In-place removal of NULL bytes, path traversal (../), and RTL override chars. Max 10 levels of recursion. |
parseIncomingRecursively(&$data, $input, $iteration) |
Returns a new sanitized array. Cleans both keys and values. Max 20 levels of recursion. |
parseCleanKey($key) |
Strips double dots, __wrapped__ patterns, applies htmlspecialchars(urldecode()). |
parseCleanValue($value) |
Strips <script tags, HTML comments, normalizes line breaks, handles entity encoding. |
Action validation¶
| Method | Description |
|---|---|
validate($action, $data) |
Returns true/false for whether $data meets minimum requirements for the given API action. |
validateOrFail($action, $data) |
Returns null if valid, or ['status' => STATUS_INVALID_INPUT, 'data' => $data] if invalid. |
confirmIDs($ids) |
Filters an array to only positive integer IDs. |
RequestManager¶
File: src/Core/Http/RequestManager.php
Static facade that stores the merged GET+POST request data. This is the primary request data access pattern used throughout the codebase.
How data gets in¶
LegacyInitializer::initCore() calls:
$rInput = InputValidator::parseIncomingRecursively($_GET, array());
RequestManager::set(InputValidator::parseIncomingRecursively($_POST, $rInput));
POST parameters override GET parameters with the same key (POST is merged on top of GET).
API¶
| Method | Description |
|---|---|
set(array $request) |
Store the entire request data array. |
getAll() |
Retrieve all stored request data. |
get(string $key, $default = null) |
Retrieve a single value by key. |
update(string $key, $value) |
Update a single key in the stored data. |
Usage¶
// Read a request parameter
$streamId = RequestManager::get('stream_id');
// Read all parameters
$allParams = RequestManager::getAll();
// Update a value (rare, used by some legacy handlers)
RequestManager::update('status', 'active');
Request¶
File: src/Core/Http/Request.php
Object-oriented request wrapper. Contains a static capture() factory and instance methods for accessing sanitized input. While the class exists and is fully functional, the primary production flow uses InputValidator + RequestManager instead. The Request class's static sanitization methods (cleanGlobals, parseIncomingRecursively) are used by LegacyInitializer::initStreaming() for backward compatibility.
Construction¶
// Static factory (singleton, not used in production admin flow)
$request = Request::capture();
// Direct construction
$request = new Request($_GET, $_POST, $_SERVER, $_COOKIE);
Instance methods¶
| Method | Signature | Description |
|---|---|---|
input |
input($key, $default = null) |
Get from merged input (POST priority over GET) |
get |
get($key = null, $default = null) |
Get from query string ($_GET). null key returns all. |
post |
post($key = null, $default = null) |
Get from POST data. null key returns all. |
all |
all() |
All merged input data |
has |
has($key) |
Check if key exists in merged input |
getInt |
getInt($key, $default = 0) |
Get value as integer |
getBool |
getBool($key, $default = false) |
Get value as boolean (via filter_var) |
server |
server($key, $default = null) |
Get $_SERVER value |
cookie |
cookie($key, $default = null) |
Get cookie value |
method |
method() |
HTTP method string (GET, POST, etc.) |
isPost |
isPost() |
Check if method is POST |
isAjax |
isAjax() |
Check X-Requested-With: XMLHttpRequest |
ip |
ip() |
Client IP (checks X-Forwarded-For, X-Real-IP, REMOTE_ADDR) |
uri |
uri() |
Request URI |
userAgent |
userAgent() |
User-Agent header |
host |
host() |
Host header (falls back to SERVER_NAME) |
rawBody |
rawBody() |
Raw POST body from php://input |
json |
json($assoc = true) |
JSON-decoded POST body |
Static sanitization methods (backward compatibility)¶
These mirror InputValidator and are used by the streaming init path:
| Method | Description |
|---|---|
cleanGlobals(&$data, $iteration) |
Same as InputValidator::cleanGlobals() |
parseIncomingRecursively(&$data, $input, $iteration) |
Same as InputValidator::parseIncomingRecursively() |
parseCleanKey($key) |
Same as InputValidator::parseCleanKey() |
parseCleanValue($value) |
Same as InputValidator::parseCleanValue() |
Router¶
File: src/Core/Http/Router.php
Singleton router for page and API dispatch. Replaces the legacy switch($rAction) pattern.
Route registration¶
| Method | Signature | Description |
|---|---|---|
get |
get($route, $handler, $options = []) |
Register a GET page route |
post |
post($route, $handler, $options = []) |
Register a POST form route |
any |
any($route, $handler, $options = []) |
Register both GET and POST for the same route |
api |
api($action, $handler, $options = []) |
Register an API route (JSON, dispatched by action name) |
group |
group($prefix, $callback, $options = []) |
Group routes under a common prefix with shared middleware/permissions |
The $handler parameter accepts:
- [ClassName::class, 'method'] -- instantiated via ServiceContainer (with fallback to new)
- A closure or callable
- [object, 'method']
The $options array supports:
- 'permission' => ['type', 'key'] -- checked via Authorization::check() before the handler runs
- 'middleware' => [callable, ...] -- array of callables executed after permission check, before handler
Route examples¶
$router = Router::getInstance();
// Simple page routes
$router->get('streams', [StreamController::class, 'index']);
$router->post('stream/save', [StreamController::class, 'save']);
// API route (JSON)
$router->api('deleteStream', [StreamController::class, 'apiDelete']);
// Grouped routes with middleware and permissions
$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],
]);
Route normalization¶
The router normalizes legacy page names by converting underscores to slashes:
| Input | Normalized |
|---|---|
watch |
watch |
watch_add |
watch/add |
settings_watch |
settings/watch |
plex_add.php |
plex/add |
This normalization is applied both at registration time (buildRoute) and at dispatch time (normalizePage), so routes registered as watch/add match page names like watch_add.
Dispatch¶
// Page dispatch (called from Public/index.php)
$router->dispatch($pageName, $method); // returns true if matched
// API dispatch (called for action= parameter)
$router->dispatchApi($action); // returns true if matched
dispatch($page, $method) execution order¶
- Normalize
$page(underscores to slashes, strip.php). - Look up in POST routes (if method is POST) or GET routes. If POST route not found, falls back to GET routes.
- Permission check via
checkPermission(). If denied, callsdenyAccess()(redirect or 403). - Middleware execution. Each callable in the
middlewarearray is called. If any returnsfalse, execution stops. - Handler invocation via
callHandler().
dispatchApi($action) execution order¶
- Look up in API routes by action name.
- Permission check. If denied, outputs
{"result": false}and exits. - Handler invocation. No middleware is executed.
Important: dispatchApi() does NOT run middleware. This is a deliberate difference from page dispatch.
When nothing matches¶
Both dispatch() and dispatchApi() return false when no route matches. Public/index.php then responds with http_response_code(404); echo '404 Not Found'; — there is no catch-all controller. (A mistyped asset path that reaches the front controller, rather than being served by nginx, lands on the same 404.)
Pitfall — two sanitization APIs + a global. Input can be reached three ways:
InputValidator(the global request-sanitization layer), theRequestclass's staticsanitize*()methods (kept for backward compatibility), and the global-staticRequestManager. They are not interchangeable and the sanitization one applies depends on the bootstrap path — pick the layer the surrounding code already uses rather than mixing them, and rememberRequestManager's static state makes it order-dependent and awkward to isolate in tests (set it explicitly in a test rather than relying on prior request state).
Module route registration¶
Modules register routes via ModuleInterface::registerRoutes(). The router supports a safe registration mode to prevent modules from overwriting core routes:
$router->beginModuleRegistration();
// Module routes registered here -- duplicates are silently skipped
$moduleLoader->bootAll($container, $router);
$router->endModuleRegistration();
// Check for collisions (logged in development mode)
$collisions = $router->drainRouteCollisions();
During module registration mode (preserveExistingRoutes = true), if a module tries to register a route that already exists, the existing route is preserved and the collision is recorded. drainRouteCollisions() returns and clears the collected collisions as an array of ['type' => 'get'|'post'|'api', 'key' => 'route/path'].
Introspection¶
| Method | Description |
|---|---|
hasRoute($page) |
Check if a page route exists (GET or POST) |
hasApiRoute($action) |
Check if an API route exists |
getRoutes() |
Return all registered route keys as ['get' => [...], 'post' => [...], 'api' => [...]] |
Response¶
File: src/Core/Http/Response.php
Static helper for sending HTTP responses. Replaces scattered header() + echo + exit() patterns.
| Method | Signature | Description |
|---|---|---|
json |
json($data, $statusCode = 200, $options = 0) |
Send JSON response and exit |
jsonError |
jsonError($message, $statusCode = 400, $extra = []) |
Send JSON error and exit |
redirect |
redirect($url, $statusCode = 302) |
Send redirect and exit |
notFound |
notFound($message = 'Not Found') |
Send 404 and exit |
header |
header($name, $value) |
Set a single response header |
cors |
cors() |
Set CORS headers (Access-Control-Allow-Origin: *) |
noCache |
noCache() |
Set no-cache headers (used for HLS playlists) |
raw |
raw($content, $contentType, $statusCode) |
Send raw content with content type and exit |
empty |
empty($statusCode = 204) |
Send empty response and exit |
Bootstrap Contexts¶
XC_Bootstrap::boot($context) provides context-dependent initialization. Each context builds on the previous:
| Context | What it initializes |
|---|---|
BootContext::Minimal |
Autoload + constants + config + Logger. No DB connection. |
BootContext::Cli |
+ Database + LegacyInitializer::initCore() (input sanitization, settings, FFmpeg paths). Optional Redis. |
BootContext::Stream |
+ Database only (lightweight, no LegacyInitializer). Streaming endpoints use StreamingRequestBootstrap instead. |
BootContext::Admin |
+ Session + Database + LegacyInitializer::initCore() + Redis + Admin API + Translator + admin globals. Full initialization. |
boot()accepts theBootContextenum (preferred). The legacy string constantsXC_Bootstrap::CONTEXT_{MINIMAL,CLI,STREAM,ADMIN}are@deprecatedaliases kept for backward compatibility — you'll still see them in older call sites. See Bootstrap Contexts for the full matrix.
All HTTP contexts (not CLI) also run flood protection and host verification before context-specific initialization.
Related files¶
| File | Purpose |
|---|---|
src/Core/Http/RequestGuard.php |
Pre-routing safety and logger init (legacy include) |
src/Core/Http/Request.php |
OOP request wrapper with sanitization methods |
src/Core/Http/Router.php |
Route registration and dispatch |
src/Core/Http/RequestManager.php |
Static request data facade (primary access pattern) |
src/Core/Http/Response.php |
Response output helpers |
src/Core/Validation/InputValidator.php |
Input sanitization and action validation |
src/Core/Init/LegacyInitializer.php |
Legacy core initialization (wires sanitization into RequestManager) |
src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php |
Lightweight streaming endpoint bootstrap |
src/Streaming/StreamingBootstrap.php |
Streaming DB connection and legacy init |
src/bootstrap.php |
Unified bootstrap (XC_Bootstrap class) |
src/Public/index.php |
Front controller for admin/reseller/player/API |
src/Public/routes/admin.php |
Admin page route definitions |
src/Public/routes/reseller.php |
Reseller page route definitions |
src/Public/routes/player.php |
Player page route definitions |