From e2814fd81cb9d5f0c23b8d5da584303ddc7f6c96 Mon Sep 17 00:00:00 2001 From: Marek Miklewicz Date: Tue, 30 Jun 2026 19:53:28 +0200 Subject: [PATCH] fix: emit full raw http responses --- exec/lib/Http.php | 95 +++++++++++++++++++++++++-------- plugin.conf | 2 +- tests/http_test.php | 12 +++++ tests/package_contents_test.php | 2 +- 4 files changed, 88 insertions(+), 23 deletions(-) diff --git a/exec/lib/Http.php b/exec/lib/Http.php index e1fd460..e150946 100644 --- a/exec/lib/Http.php +++ b/exec/lib/Http.php @@ -37,23 +37,26 @@ final class Http public static function redirectNoStore(string $url): void { - header('Cache-Control: no-store, private'); - header('Pragma: no-cache'); - header('Referrer-Policy: no-referrer'); - header('X-Content-Type-Options: nosniff'); - header('Location: ' . $url, true, 302); + self::sendResponse(302, [ + 'Cache-Control' => 'no-store, private', + 'Pragma' => 'no-cache', + 'Referrer-Policy' => 'no-referrer', + 'X-Content-Type-Options' => 'nosniff', + 'Location' => $url, + ], ''); exit; } public static function browserRedirectNoStore(string $url): void { - header('Content-Type: text/html; charset=UTF-8'); - header('Cache-Control: no-store, private'); - header('Pragma: no-cache'); - header('Referrer-Policy: no-referrer'); - header('X-Content-Type-Options: nosniff'); - header("Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"); - echo self::redirectPageHtml($url); + self::sendResponse(200, [ + 'Content-Type' => 'text/html; charset=UTF-8', + 'Cache-Control' => 'no-store, private', + 'Pragma' => 'no-cache', + 'Referrer-Policy' => 'no-referrer', + 'X-Content-Type-Options' => 'nosniff', + 'Content-Security-Policy' => "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'", + ], self::redirectPageHtml($url)); exit; } @@ -78,16 +81,66 @@ final class Http public static function errorPage(string $message, int $status): void { - http_response_code($status); - header('Content-Type: text/html; charset=UTF-8'); - header('Cache-Control: no-store, private'); - header('Referrer-Policy: no-referrer'); $safe = htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - echo ''; - echo ''; - echo 'Serwer poczty'; - echo '

Serwer poczty

' . $safe . '

'; - echo ''; + $body = '' + . '' + . 'Serwer poczty' + . '

Serwer poczty

' . $safe . '

' + . ''; + self::sendResponse($status, [ + 'Content-Type' => 'text/html; charset=UTF-8', + 'Cache-Control' => 'no-store, private', + 'Referrer-Policy' => 'no-referrer', + ], $body); exit; } + + /** + * @param array $headers + */ + public static function rawHttpResponse(int $status, array $headers, string $body): string + { + $headers['Content-Length'] = (string)strlen($body); + $lines = ['HTTP/1.1 ' . $status . ' ' . self::reasonPhrase($status)]; + foreach ($headers as $name => $value) { + $safeName = preg_replace('/[^A-Za-z0-9-]/', '', (string)$name) ?? ''; + if ($safeName === '') { + continue; + } + $safeValue = str_replace(["\r", "\n"], '', (string)$value); + $lines[] = $safeName . ': ' . $safeValue; + } + return implode("\r\n", $lines) . "\r\n\r\n" . $body; + } + + /** + * @param array $headers + */ + private static function sendResponse(int $status, array $headers, string $body): void + { + if (PHP_SAPI === 'cli') { + echo self::rawHttpResponse($status, $headers, $body); + return; + } + + http_response_code($status); + foreach ($headers as $name => $value) { + header($name . ': ' . $value); + } + header('Content-Length: ' . strlen($body)); + echo $body; + } + + private static function reasonPhrase(int $status): string + { + return match ($status) { + 200 => 'OK', + 302 => 'Found', + 403 => 'Forbidden', + 405 => 'Method Not Allowed', + 500 => 'Internal Server Error', + 502 => 'Bad Gateway', + default => 'OK', + }; + } } diff --git a/plugin.conf b/plugin.conf index 9d638fb..89d7d09 100644 --- a/plugin.conf +++ b/plugin.conf @@ -2,7 +2,7 @@ name=mail-login id=mail-login type=user author=HITME.PL -version=1.0.10 +version=1.0.11 active=no installed=no user_run_as=root diff --git a/tests/http_test.php b/tests/http_test.php index 0a1eb1f..42cda2d 100644 --- a/tests/http_test.php +++ b/tests/http_test.php @@ -12,3 +12,15 @@ test('http redirect page performs top level browser navigation', function (): vo assert_contains('https://mx1.domena.pl:2222/api/login/url?key=SECRET&x=1', $html); assert_contains('https://mx1.domena.pl:2222/api/login/url?key=SECRET\\u0026x=1', $html); }); + +test('http raw response is a complete http response for directadmin proxy', function (): void { + $response = Http::rawHttpResponse(200, [ + 'Content-Type' => 'text/html; charset=UTF-8', + 'Cache-Control' => 'no-store, private', + ], ''); + + assert_contains("HTTP/1.1 200 OK\r\n", $response); + assert_contains("Content-Type: text/html; charset=UTF-8\r\n", $response); + assert_contains("Content-Length: 28\r\n", $response); + assert_contains("\r\n\r\n", $response); +}); diff --git a/tests/package_contents_test.php b/tests/package_contents_test.php index 916d381..b8d1e85 100644 --- a/tests/package_contents_test.php +++ b/tests/package_contents_test.php @@ -8,7 +8,7 @@ test('plugin metadata and packaging docs match project rules', function (): void assert_contains('name=mail-login', $conf); assert_contains('id=mail-login', $conf); assert_contains('type=user', $conf); - assert_contains('version=1.0.10', $conf); + assert_contains('version=1.0.11', $conf); assert_contains('user_run_as=root', $conf); assert_contains('/Users/marek/GPT/ChatGPT/da_plugins/mail-login/src', $packing); assert_contains('/Users/marek/GPT/ChatGPT/da_plugins/mail-login/archives/X.Y.Z/mail-login.tar.gz', $packing);