diff --git a/src/Cli/Commands/StartupCommand.php b/src/Cli/Commands/StartupCommand.php index 084cae03..d62bddbf 100644 --- a/src/Cli/Commands/StartupCommand.php +++ b/src/Cli/Commands/StartupCommand.php @@ -76,6 +76,11 @@ class StartupCommand implements CommandInterface { @chmod($rRunSh, 0755); } + // Core scripts/binaries lose their executable bit on a mode-dropping + // deploy, and a missing php-fpm pool config leaves that worker down. + self::ensureExecutableScripts(['service', 'update', 'bin/redis/redis-server', 'bin/daemons.sh'], MAIN_HOME); + self::ensurePhpFpmPoolConfigs(MAIN_HOME); + // ── Установка crontab и запуск кэша ────────────────── if (posix_getpwuid(posix_geteuid())['name'] == 'root') { $this->installRootCrontab(); @@ -94,6 +99,44 @@ class StartupCommand implements CommandInterface { return 0; } + /** + * Restore the executable bit on core scripts that a mode-dropping deploy + * (git archive, some rsync flags) can strip. + * + * @param list $rScripts Paths relative to $rBase. + */ + private static function ensureExecutableScripts(array $rScripts, string $rBase): void { + foreach ($rScripts as $rScript) { + $rPath = $rBase . $rScript; + if (file_exists($rPath) && !is_executable($rPath)) { + @chmod($rPath, 0755); + } + } + } + + /** + * Regenerate any missing php-fpm pool config (1..4.conf) from the template, + * so a worker whose config was lost comes back on the next boot. + */ + private static function ensurePhpFpmPoolConfigs(string $rBase): void { + $rTemplatePath = $rBase . 'bin/php/etc/template'; + if (!file_exists($rTemplatePath)) { + return; + } + $rTemplate = (string) file_get_contents($rTemplatePath); + foreach (range(1, 4) as $rID) { + $rConf = $rBase . 'bin/php/etc/' . $rID . '.conf'; + if (file_exists($rConf)) { + continue; + } + file_put_contents($rConf, str_replace('#PATH#', $rBase, str_replace('#ID#', (string) $rID, $rTemplate))); + @chmod($rConf, 0644); + if (posix_geteuid() === 0) { + exec('sudo chown xc_vm:xc_vm ' . escapeshellarg($rConf) . ' 2>/dev/null'); + } + } + } + private function installRootCrontab(): void { $rCrons = []; $rCrons[] = '* * * * * ' . PHP_BIN . ' ' . MAIN_HOME . 'console.php cron:root_signals # XC_VM'; diff --git a/src/Streaming/Auth/StreamAuthMiddleware.php b/src/Streaming/Auth/StreamAuthMiddleware.php index 080d5db2..99319495 100644 --- a/src/Streaming/Auth/StreamAuthMiddleware.php +++ b/src/Streaming/Auth/StreamAuthMiddleware.php @@ -43,8 +43,8 @@ class StreamAuthMiddleware { header('Alt-Svc: h3-29=":' . $rServers[SERVER_ID]['https_broadcast_port'] . '"; ma=2592000,h3-T051=":' . $rServers[SERVER_ID]['https_broadcast_port'] . '"; ma=2592000,h3-Q050=":' . $rServers[SERVER_ID]['https_broadcast_port'] . '"; ma=2592000,h3-Q046=":' . $rServers[SERVER_ID]['https_broadcast_port'] . '"; ma=2592000,h3-Q043=":' . $rServers[SERVER_ID]['https_broadcast_port'] . '"; ma=2592000,quic=":' . $rServers[SERVER_ID]['https_broadcast_port'] . '"; ma=2592000; v="46,43"'); } - if (empty($rSettings['send_unique_header_domain']) && !filter_var(HOST, FILTER_VALIDATE_IP)) { - $rSettings['send_unique_header_domain'] = '.' . HOST; + if (empty($rSettings['send_unique_header_domain']) && ($rDomain = self::uniqueHeaderDomain()) !== null) { + $rSettings['send_unique_header_domain'] = $rDomain; } if (!empty($rSettings['send_unique_header'])) { @@ -53,6 +53,19 @@ class StreamAuthMiddleware { } } + /** + * The leading-dot cookie domain derived from the request host, or null when + * there is no usable host name (HOST undefined, empty, or a bare IP — a dot + * prefix on which is not a valid cookie domain). + */ + private static function uniqueHeaderDomain(): ?string { + $rHost = defined('HOST') ? (string) HOST : (string) ($_SERVER['HTTP_HOST'] ?? ''); + if ($rHost === '' || filter_var($rHost, FILTER_VALIDATE_IP)) { + return null; + } + return '.' . $rHost; + } + /** * Дешифровка токена и базовая валидация (формат + срок действия). * diff --git a/tests/Unit/StartupCommandRuntimeFilesTest.php b/tests/Unit/StartupCommandRuntimeFilesTest.php new file mode 100644 index 00000000..0ebb4518 --- /dev/null +++ b/tests/Unit/StartupCommandRuntimeFilesTest.php @@ -0,0 +1,54 @@ +setAccessible(true); + return $rM->invoke(null, ...$rArgs); + } + + protected function setUp(): void { + $this->dir = sys_get_temp_dir() . '/xcvm_startup_' . uniqid() . '/'; + mkdir($this->dir . 'bin/php/etc', 0777, true); + } + + protected function tearDown(): void { + exec('rm -rf ' . escapeshellarg($this->dir)); + } + + public function testEnsureExecutableScriptsSetsBitAndSkipsMissing(): void { + file_put_contents($this->dir . 'service', "#!/bin/sh\n"); + chmod($this->dir . 'service', 0644); + + $this->call('ensureExecutableScripts', ['service', 'does-not-exist'], $this->dir); + + $this->assertTrue(is_executable($this->dir . 'service')); + } + + public function testEnsurePhpFpmPoolConfigsGeneratesOnlyMissing(): void { + file_put_contents($this->dir . 'bin/php/etc/template', 'id=#ID# path=#PATH#'); + file_put_contents($this->dir . 'bin/php/etc/1.conf', 'KEEP'); // existing → untouched + + $this->call('ensurePhpFpmPoolConfigs', $this->dir); + + $this->assertSame('KEEP', file_get_contents($this->dir . 'bin/php/etc/1.conf')); + $this->assertSame('id=2 path=' . $this->dir, file_get_contents($this->dir . 'bin/php/etc/2.conf')); + $this->assertFileExists($this->dir . 'bin/php/etc/4.conf'); + } + + public function testEnsurePhpFpmPoolConfigsWithoutTemplateIsNoop(): void { + $this->call('ensurePhpFpmPoolConfigs', $this->dir); + $this->assertFileDoesNotExist($this->dir . 'bin/php/etc/1.conf'); + } +} diff --git a/tests/Unit/StreamAuthMiddlewareHostTest.php b/tests/Unit/StreamAuthMiddlewareHostTest.php new file mode 100644 index 00000000..072f91bf --- /dev/null +++ b/tests/Unit/StreamAuthMiddlewareHostTest.php @@ -0,0 +1,40 @@ +setAccessible(true); + return $rM->invoke(null); + } + + protected function setUp(): void { + if (defined('HOST')) { + $this->markTestSkipped('HOST constant already defined; $_SERVER path not reachable'); + } + } + + public function testHostNameYieldsDottedDomain(): void { + $_SERVER['HTTP_HOST'] = 'cdn.example.com'; + $this->assertSame('.cdn.example.com', $this->domain()); + } + + public function testBareIpYieldsNull(): void { + $_SERVER['HTTP_HOST'] = '203.0.113.9'; + $this->assertNull($this->domain()); + } + + public function testEmptyHostYieldsNull(): void { + $_SERVER['HTTP_HOST'] = ''; + $this->assertNull($this->domain()); + } +}